Syksyn 2025 koe
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:
aputulosx
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)