Kokeen korjaus on edelleen kesken…

Omasta arvioinnista voi tarvittaessa kysyä suoraan kunkin tehtävän arvijoijalta.

Tehtävä 1

Tehtävän arvioi Tony Lam. Omasta arvioinnista voi tarvittaessa kysyä etunimi.sukunimi@helsinki.fi tai Discordissa

Tehtävä 2

Tehtävän arvioi Matti Luukkainen. Omasta arvioinnista voi tarvittaessa kysyä etunimi.sukunimi@helsinki.fi tai Discordissa

Kurssimateriaalin (mm. INVEST-kriteerit) mukaan hyvä user story on

  • kuvaa käyttäjälle arvoa tuottavan toiminnallisuuden,
  • on asiakkaan kannalta ymmärrettävällä kielellä (ei teknistä jargonia),
  • on tarpeeksi pieni (toteutettavissa yhdessä sprintissä),
  • kuvaa järjestelmän toiminnallisuutta päästä päähän, ei vain tietoteknistä kerrosta,
  • testattavissa
  • neuvoteltavissa, eli ei liian tiukasti kiinnitetty toteutukselta
  • työmäärä on arvioitavissa
  • sisältää hyväksymiskriteerit

Ensimmäinen esimerkkistory

Ylläpitäjän tulee saada mahdollisuus suorittaa kaikki CRUD-operaatiot (eli luonti, listaus, muokkaus, poisto) PostgreSQL:n tallennettaville avoimen yliopiston kurssitoteutuksille. Näitä varten kantaan tehdä oma taulu, nimeltään esim. OPEN_UNI_COURSES. Operaatioiden tulee olla riittävän nopeita.

sisältää useita ongelmia

  • Ei ole kirjoitettu asiakkaan kielellä
  • Story on myös liian laaja
  • On osin sisällöltään vaikea arvioida valmiiksi (…riittävän nopeita)
  • Ei sisällä hyväksymäkriteerejä

Storyistä toinen

Tarvitaan AI-pohjainen mekanismi automaattisten tiedekunta- ja koulutusohjelmakohtaisten kurssipalautekoosteiden muodostamiseen

Story ei noudata kaikkia INVEST-kriteerejä, mutta toisaalta hyvän backlogin DEEP-kriteerit toteavat, että storyn tulee olla sopivan tarkka. Koska kyseessä on pitkän ajan päästä toteutettavaksi tuleva story, on sen nykyinen tarkkuus hyvä.

Alustava pisteytys:

Ensimmäinen stroty 4 pistettä (0.5 pistettä per puute/korjaus):

  • Ei asiakkaan kielellä
  • Liian iso
  • Valmius vaikea arvioida
  • Hyväksymäkriteerien puute

Lisäksi 0.5 jos todettiin eksplisiittisesti, että story ei ole negotiable.

Toinen story 2 pistettä:

  • perusteet sille miksi story on hyvä

Näiden lisäksi hyvistä oivalluksista tai havainnoista sai plussa, esim. INVEST-kriteerien maininta 0.5p

Tehtävä 3

Tehtävän arvioi Nea Kovalainen. Omasta arvioinnista voi tarvittaessa kysyä etunimi.sukunimi@helsinki.fi tai Discordissa

Tehtävä 4

Tehtävän arvioi Heidi Tapani. Omasta arvioinnista voi tarvittaessa kysyä etunimi.sukunimi@helsinki.fi tai Discordissa

Tehtävä 5

Tehtävän arvioi Rasmus Viitanen. Omasta arvioinnista voi tarvittaessa kysyä etunimi.sukunimi@helsinki.fi tai Discordissa

a) Mitä cross functional -tiimeillä ja feature-tiimeillä tarkoitetaan? (1p)

  • Cross functional -tiimi on tiimi, jossa on yhdessä kaikki tarvittava osaaminen tuotteen tai ominaisuuden viemiseksi ideasta tuotantoon.
  • Feature-tiimi on tämän laajempi, asiakkaalle arvoa tuottavien kokonaisuuksien ympärille rakentuva cross functional -tiimi.

Pisteytys:

Cross functional -tiimin oikea määritelmä: 0,5 p
Feature-tiimin oikea määritelmä: 0,5 p
Yhteensä max 1 p

b) Millä perusteilla tällaisia tiimejä pidetään hyvinä? (3p)

  • Vähentää riippuvuutta muista tiimeistä, mikä eliminoi monta Lean hukkaa (eli asiakkaalle arvoa tuottamatonta valmistusprosessin tekijää).
    • Ei turhaa odotusta (esim. tarvetta odottaa toisen tiimin tekemää backendia).
    • Eliminoi välivarastoja (esim. feature on koodattu, mutta odottaa integrointia muuhun koodikantaan).
    • Vikojen korjaaminen ei viivästy, kun tiimi vastaa itse laadunhallinnasta .
  • Mahdollistaa valmiin, testatun ja julkaistavan tuotteen osan (shippable product increment) jokaisen sprintin aikana.
  • Tiimi voi päättää itse työtavoistaan ja vastaa tekemisestään yhdessä.
  • Vähentää siiloutumista ja mahdollistaa oppimista yli kompetenssirajojen.
  • Kun tiimi vastaa suoraan asiakkaalle arvoa tuottavasta toiminnallisuudesta, sillä on selkeä omistajuus työnsä lopputuloksesta ja sen laadusta.

Pisteytys:

1 p per oikea ja kurssimateriaalin mukainen perustelu, kuitenkin enintään 3 p.
Myös muut vastaavat, hyvin perustellut näkökulmat on huomioitu arvostelussa.

c) Mitä mahdollisia huonoja puolia cross functional -tiimien käyttämisestä voi olla? (2p)

  • Hyvät käytänteet eivät välttämättä leviä tiimien välillä.
  • Autonomiset tiimit voivat johtaa siihen, että samoja ongelmia ratkotaan useita kertoja eri tiimeissä ilman tiedon jakamista.
  • Tiimien autonomisuus voi johtaa epäyhtenäisiin toimintatapoihin, mikä voi vaikuttaa kokonaisuuden hallintaan.
    • Cross functional -tiimimalli vaatii tiimiltä kypsyyttä ja yhteisiä toimintatapoja; ilman niitä työn organisointi ja koordinointi voivat kärsiä.
  • Tietyn erikoisosaamisen omaavat henkilöt (esim. UX-suunnittelu, tietoturva-osaaminen, infrastruktuuriosaaminen) voivat ajoittain olla alikuormitettuja yhdessä tiimissä, samalla kun toisissa tiimeissä on pulaa samasta erityisosaamisesta.
  • Cross functional -tiimien kokoaminen voi olla haastavaa, koska samaan tiimiin on koottava useita eri osaamisalueita, eikä sopivia resursseja ole aina helposti saatavilla.

Pisteytys: 1 p per oikea ja hyvin perusteltu huono puoli, kuitenkin enintään 2 p.
Myös mallivastauksen ulkopuoliset, hyvin perustellut havainnot on huomioitu arvostelussa.

Tehtävä 6

Tehtävän arvioi Jasse Merivirta. Omasta arvioinnista voi tarvittaessa kysyä etunimi.sukunimi@helsinki.fi tai Discordissa

1. Tiedostonkäsittelyn eriytys omaksi luokaksi 1.5 pistettä

Esimerkki:

class OrganisationRepository:
    def __init__(self, filename):
        self._filename = filename

    def get_clubs(self):
        clubs = []
        try:
            with open(self._filename) as file:
                for row in file:
                    parts = row.strip().split(";")
                    clubs.append({
                        "name": parts[0],
                        "university": parts[1],
                        "founded": int(parts[2]),
                        "colors": parts[3].split(","),
                    })
            return clubs
        except:
            return []

    def save_clubs(self, clubs):
        with open(self._filename, "w") as file:
            for club in clubs:
                colors_str = ",".join(club["colors"])
                file.write(f"{club['name']};{club['university']};{club['founded']};{colors_str}\n")

1 piste jos toteutettu metodina. Tällöin myöskään seuraavan kohdan injektoinnista ei voi saada täysiä pisteitä.

2. Riippuvuuksien injektointi 1.5 pistettä

Tarkoituksena injektoida eriytetty I/O pääluokalle ja poistaa kovakoodattu filepath.

Esimerkki:

class OrganisationRegister:
    def __init__(self, repository):
        self._repository = repository
        self._clubs = repository.get_clubs()

3. DRY hakumetodeissa 1.5 pistettä

find_by_color, find_by_founded ja find_by_university käyttävät copy-pastettua koodia, tälle on hyvä luoda apufunktio.

Esimerkki:

def _find_club_name_by(self, criteria):
    return [club["name"] for club in self._clubs if criteria(club)]

def find_by_color(self, color):
    return self._find_club_name_by(lambda club: color in club["colors"])

def find_by_founded(self, year, criteria="=="):
    criterias = {
        ">": lambda club: club["founded"] > year,
        "<": lambda club: club["founded"] < year,
        "==": lambda club: club["founded"] == year,
    }
    return self._find_club_name_by(criterias[criteria])

def find_by_university(self, university):
    return self._find_club_name_by(lambda club: club["university"] == university)

4. Yhden organisaation haulle apufunktio 1 piste

Esimerkki:

def _find_by_name(self, name):
    for club in self._clubs:
        if club["name"] == name:
            return club
    return None

def print_info(self, name):
    club = self._find_by_name(name)
    if not club:
        print(f"{name} not found")
        return

    print(f'{club["name"]} ({club["founded"]})')
    print(f'{club["university"]}')
    print(f'Haalarin värit: {", ".join(club["colors"])}')

Näin yhden organisaation haku on eriytetty siitä, mitä sillä tehdään.


5. Parannettu nimeäminen 1 piste

Koodissa on suomenkielisiä ja epäselviä muuttujanimiä, jotka on hyvä siistiä.

Huonoja:

  • apu
  • tulos
  • x

Esimerkki::

# Kuvaavampi muuttujanimi
for club in clubs:
    colors_str = ",".join(club["colors"])

# Samoin tässä
existing_club = self._find_by_name(name)

# Selkeämpi lambda parametri
lambda club: color in club["colors"]

Malliratkaisu kokonaisuudessaan:


class OrganisationRepository:
    def __init__(self, filename):
        self._filename = filename

    def get_clubs(self):
        clubs = []
        try:
            with open(self._filename) as file:
                for row in file:
                    parts = row.strip().split(";")
                    clubs.append(
                        {
                            "name": parts[0],
                            "university": parts[1],
                            "founded": int(parts[2]),
                            "colors": parts[3].split(","),
                        }
                    )
            return clubs
        except:
            return []

    def save_clubs(self, clubs):

        with open(self._filename, "w") as file:
            for club in clubs:
                colors_str = ",".join(club["colors"])
                file.write(f"{club['name']};{club['university']};{club['founded']};{colors_str}\n")


class OrganisationRegister:
    def __init__(self, repository):
        self._repository = repository
        self._clubs = repository.get_clubs()

    def _find_club_name_by(self, criteria):
        return [club["name"] for club in self._clubs if criteria(club)]

    def find_by_color(self, color):
        return self._find_club_name_by(lambda club: color in club["colors"])

    def find_by_founded(self, year, criteria="=="):
        criterias = {
            ">": lambda club: club["founded"] > year,
            "<": lambda club: club["founded"] < year,
            "==": lambda club: club["founded"] == year,
        }

        return self._find_club_name_by(criterias[criteria])

    def find_by_university(self, university):
        return self._find_club_name_by(lambda club: club["university"] == university)

    def _find_by_name(self, name):
        for club in self._clubs:
            if club["name"] == name:
                return club

        return None

    def print_info(self, name):
        club = self._find_by_name(name)
        if not club:
            print(f"{name} not found")
            return

        print(f'{club["name"]} ({club["founded"]})')
        print(f'{club["university"]}')
        print(f'Haalarin värit: {", ".join(club["colors"])}')

    def add_club(self, name, university, founded, colors):
        existing_club = self._find_by_name(name)
        if existing_club:
            return

        club_to_add = {
            "name": name,
            "university": university,
            "founded": founded,
            "colors": colors,
        }
        self._clubs.append(club_to_add)
        self._repository.save_clubs(self._clubs)