Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 9.1: Tekstipeli ja koodin laatu
Tästä sivusta:
Pääkysymyksiä: Saisinko vielä esimerkin ohjelmakokonaisuudesta, jossa usea luokka toimii yhteen? Miten teen ohjelmakoodistani paremmin jatkokehityskelpoisen?
Mitä käsitellään? Eräitä ohjelman suunnittelun periaatteita. Vähän ohjelman muokattavuudesta ja refaktoroinnista. Lisäharjoitusta hakurakenteista.
Mitä tehdään? Ensin luetaan, sitten ohjelmoidaan. Valmistaudutaan tiedollisesti ja henkisesti luvun 10.1 isoon ja vapaamuotoiseen ohjelmointitehtävään.
Suuntaa antava työläysarvio:? Kolme, neljä tuntia.
Pistearvo: B80.
Johdanto: metsäseikkailu
Oheismoduulista AdventureDraft löytyy tekstipeli eli peli, jossa pelaaja ohjaa pelihahmoa
kirjoitetuin komennoin ja jossa pelimaailmakin on esitetty tekstimuodossa. Käynnistä
o1.adventure.draft.ui.AdventureTextUI
, jolloin peli toimii tekstikonsolissa tähän tapaan:
Pelaa peliä hetki. Voit antaa komentoja kuten go north, go west ja rest. Komento quit lopettaa pelin.
Jos haluat, löydät pelimaailman kartan moduulin tiedostosta forest_map.gif
.
Huomaat, että peli kyllä toimii, mutta sen viihdearvo on vähäinen. Tämän pääset itse korjaamaan luvun 10.1 ohjelmointitehtävässä. Sitä ennen tässä luvussa tutkitaan valmiina annettua tekstipelitoteutusta ja parannetaan sen koodin laatua refaktoroimalla.
AdventureDraft-moduulista
Tutustu nyt itsenäisesti luokkien koodiin.
Alempana tässä luvussa oletetaan, että AdventureDraft-pelin toteutus on suunnilleen tuttu! Lue sen ohjelmakoodi läpi. Jokaista yksityiskohtaa ei tarvitse vielä ymmärtää, mutta sinun tulisi hahmottaa, mikä kunkin luokan ja metodin perusidea on.
Yksi hyvä tapa tutustua ohjelmaan on ajaa koodia debuggerissa rivi riviltä. Tämä kaavio luokkien suhteista voi myös auttaa.
Jatka vasta, kun olet tutustunut moduuliin. Koodi kannattaa pitää esillä tätä lukua lukiessasi.
Tavoitteita
Mitä jos haluaisimme:
- lisätä peliin uuden alueen,
- lisätä peliin pari uutta suuntaa, joihin voi mennä (esim. käskyllä go up voisi kiivetä puuhun), ja
- tarjota pelaajalle graafisen käyttöliittymän, joka toimii omassa ikkunassaan kuten alla olevassa kuvassa?
Tarkastellaan kutakin kohtaa erikseen. Osoittautuu, ettei nykymuotoinen koodi taivu näihin kaikkiin tarkoituksiin kovinkaan hyvin.
Alueen lisääminen
Yksittäisen alueen lisääminen peliin onnistuu vielä hyvin. Lisätään alueen tiedot
Adventure
-luokkaan esimerkiksi näin:
val northPole = new Area("The North Pole", "You find yourself at the North Pole. BRRR!")
northForest.northNeighbor = Some(northPole)
northPole.southNeighbor = Some(northForest)
Suuntien lisääminen
Suuntien — vaikkapa up ja down —- lisääminen onkin jo hankalampaa. Osoittautuu, että useita kohtia ohjelmasta pitää muuttaa.
- Tietysti ainakin
Adventure
-luokan alkua on aina muutettava, kun pelin kartta muuttuu. Tuohon luokkaanhan kirjataan, mitä alueita pelissä on ja miltä alueelta pääsee mihinkin suuntaan. Area
-luokkaan pitää lisätä uudet ilmentymämuuttujatupNeighbor
jadownNeighbor
.- Jotta pelaajahahmoa voi liikuttaa näihin uusiin suuntiin, pitää
Player
-luokango
-metodiin lisätä pari uuttaif
-käskyä:if (direction == "up")
jne. - Jotta ylä- ja alasuuntakin tulostuvat alueen kuvauksessa
mahdollisiksi liikkumasuunniksi, pitää
Adventure
-luokanprintAreaInfo
-metodiin lisätä pari uuttaif
-käskyä:if (area.upNeighbor.isDefined) print(" up")
jne.
Ei hyvä. Ohjelman muokattavuuden kannalta olisi suotavaa, että muutokset ja lisäykset vaikuttavat vain harvaan eri paikkaan.
Lisäksi koodin toisteisuus sen kuin pahenee suuntia lisätessä.
Ongelmat johtuvat siitä, miten alueiden naapuruussuhteisiin liittyvä logiikka on siroteltu pitkin poikin ohjelmakoodia.
Refaktoroidaan koodia vaiheittain.
Ensimmäinen refaktorointi: parametreja Area
-luokan metodeihin
Kootaan naapuruuksien käsittely Area
-luokkaan, onhan kyse nimenomaan alueiden
ominaisuuksista. Aloitetaan kitkemällä naapuruussuuntien luetteleminen Player
-luokasta,
jonka go
-metodi näyttää nyt tältä.
def go(direction: String) = {
var destination: Option[Area] = None
if (direction == "north") {
destination = this.location.northNeighbor
} else if (direction == "east") {
destination = this.location.eastNeighbor
} else if (direction == "south") {
destination = this.location.southNeighbor
} else if (direction == "west") {
destination = this.location.westNeighbor
}
// ...
Emme haluaisi joutua muuttamaan Player
-luokkaa pelkän suunnanlisäyksen vuoksi. Niinpä
siellä ei saisi olla sellaista koodia, jossa luetellaan kaikki ilmansuunnat.
Pääsemme kyllä eroon suuntien mainitsemisesta. Sen sijaan, että kysyisimme alueoliolta sen "pohjoisnaapuria", "itänaapuria" jne., kysytään siltä "naapuria suunnassa X":
def go(direction: String) = {
val destination = this.location.neighbor(direction)
// ...
Koodi lyheni, ja toisto sekä maininnat yksittäisistä suunnista hävisivät. Metodia go
ei
enää tarvitse muuttaa, jos peliin lisätään suuntia. Hienoa!
Nyt tietysti tarvittaisiin Area
-luokkaan tuollainen metodi nimeltä neighbor
. Tässä
ensin sivistymätön toteutus:
class Area(var name: String, var description: String) {
private var northNeighbor: Option[Area] = None
private var eastNeighbor: Option[Area] = None
private var southNeighbor: Option[Area] = None
private var westNeighbor: Option[Area] = None
def neighbor(direction: String) =
if (direction == "north")
this.northNeighbor
else if (direction == "east")
this.eastNeighbor
else if (direction == "south")
this.southNeighbor
else if (direction == "west")
this.westNeighbor
else
None
Lisätieto
Erilaisista refaktoroinneista käytetään joskus nimiä; esimerkiksi tässä tehdystä voidaan käyttää nimeä parameterize method. Jos aihe kiinnostaa, löydät internetistä pitkän listan erilaisia nimettyjä refaktorointeja.
Tämä toteutus on parempi kuin alkuperäinen, mutta ei ihan kelpaa meille sekään. Valittamisen varaa nimittäin jää. Erityisesti:
- Metodissa
neighbor
(yllä) on edelleen toisteinen eri suuntien luettelo. Onneksi se on sentäänArea
-luokan sisällä, joten suuntia lisätessä muutos tulee tähän luokkaan. Area
-luokkaan täytyy yhä lisätä ilmentymämuuttuja (upNeighbor
tms.) jokaiselle uudelle suunnalle.Adventure
-luokan toteutus ei yksinkertaistunut lainkaan, ja uusien suuntien lisääminen edellyttää edelleen senprintAreaInfo
-metodin muuttamista.
Kahdesta ensimmäisestä valituksesta pääsemme eroon jo tutuksi tulleella konstilla.
Toinen refaktorointi: toisto pois hakurakenteella
Yllä määritellyn neighbor
-metodin tehtävänä on hakea suuntaa (merkkijonoa) vastaava
Area
-olio. Kuulostaa hakurakenteelta?
import scala.collection.mutable.Map
class Area(var name: String, var description: String) {
private val neighbors = Map[String, Area]()
def neighbor(direction: String) = this.neighbors.get(direction)
get
-metodilla saadaan Option[Area]
-tyyppinen arvo, joten
neighbor
-metodi toimii juuri niin kuin haluttiinkin.Tämä toteutus on yksinkertaisempi ja helpommin muokattava. Luettelot ilmansuunnista
poistuivat koodista. neighbor
-metodi toimii yhtä hyvin mille tahansa merkkijonolle,
jota käytetään suunnan nimenä.
Abstraktioista
Äskeiset refaktoroinnit perustuvat abstraktioon: ei käsitellä yksittäistapauksia vaan yleinen tapaus. Tämä abstraktio on luonnollinen. Ajattelemmehan itsekin esimerkiksi pelaajahahmon liikuttamisesta mieluummin "päädytään naapurialueelle suunnassa X" eikä "jos suunta on se-ja-se, otetaan se naapuri, jos taas...".
Kolmas refaktorointi: naapurien asettaminen
Emme vielä käsitelleet sitä, miten alueille asetetaan naapurit, kun pelikartta muodostetaan.
Alkuperäisessä Adventure
-luokassa on tällaista koodia:
middle.northNeighbor = Some(northForest)
middle.eastNeighbor = Some(tangle)
middle.southNeighbor = Some(southForest)
middle.westNeighbor = Some(clearing)
northForest.eastNeighbor = Some(tangle)
northForest.southNeighbor = Some(middle)
northForest.westNeighbor = Some(clearing)
// jne.
Tuo koodi ei toimi yhteen uusitun Area
-luokkamme kanssa, josta northNeighbor
ja
kumppanit on poistettu. Määritellään uusittuun Area
-luokkaan metodi, joka lisää
hakurakenteeseen naapuruussuhdetta kuvaavan avain–arvo-parin:
def setNeighbor(direction: String, neighbor: Area) = {
this.neighbors += direction -> neighbor
}
Nyt Adventure
-luokassakin päästään aiempaa vähän helpommalla. Voimme kutsua
setNeighbor
-metodia ja jättää Option
-kääreet pois:
middle.setNeighbor("north", northForest)
middle.setNeighbor("east", tangle)
middle.setNeighbor("south", southForest)
middle.setNeighbor("west", clearing)
northForest.setNeighbor("east", tangle)
northForest.setNeighbor("south", middle)
northForest.setNeighbor("west", clearing)
// jne.
Huomaa: Uusia suuntia voi lisätä peliin jopa muuttamatta Area
-luokkaa. Riittää, kun
kutsumme setNeighbor
-metodia uudenlaisella merkkijonoparametrilla.
Miten lisäisit peliin puussa sijaitsevan mökin, johon voi kiivetä? (Vastaus jääköön lisäharjoitustehtäväksi.)
Yllä esitetty toteutus naapurien asettamiseksi on jo hyvä, mutta namutellaan vielä vähän. Koska pelin kartta muodostetaan kerralla, olisi kiva lisätä useita naapureita yhdellä metodikutsulla. Koko kartan voisi määritellä vaikkapa näin:
middle.setNeighbors(Vector("north" -> northForest, "east" -> tangle, "south" -> southForest, "west" -> clearing ))
northForest.setNeighbors(Vector( "east" -> tangle, "south" -> middle, "west" -> clearing ))
southForest.setNeighbors(Vector("north" -> middle, "east" -> tangle, "south" -> southForest, "west" -> clearing ))
clearing.setNeighbors(Vector("north" -> northForest, "east" -> middle, "south" -> southForest, "west" -> northForest))
tangle.setNeighbors(Vector("north" -> northForest, "east" -> home, "south" -> southForest, "west" -> northForest))
home.setNeighbors(Vector( "west" -> tangle ))
setNeighbors
-niminen metodi, ...Tuokin järjestyy, kun määritellään Area
-luokkaan metodi setNeighbors
:
def setNeighbors(exits: Vector[(String, Area)]) = {
for (exit <- exits) {
this.neighbors += exit
}
}
String
- ja
Area
-olioiden muodostamia pareja.neighbors
-hakurakenteeseen.Vaihtoehtoisia toteutustapoja
Mainittakoon lisäkikkana, että muuttuvatilaisilla hakurakenteilla
on myös metodi ++=
, joka lisää kokoelmallisen avain–arvo-pareja
kerralla. setNeighbors
-metodin saa toteutettua myös näin:
def setNeighbors(exits: Vector[(String, Area)]) = {
this.neighbors ++= exits
}
Ja lisälisäkikkana, että tähtimerkkiä *
käyttäen voi määritellä
metodin, joka ottaa mielivaltaisen määrän parametreja. Tämäkin
toimii:
def setNeighbors(exits: (String, Area)*) = {
this.neighbors ++= exits
}
exits
on tässä jälkimmäisessä toteutuksessa säiliö, jossa on
kaikki annetut (String, Area)
-tyyppiset parametriarvot. Tässä
ratkaisumallissa myös Adventure
-luokan kartanmäärityskoodi
yksinkertaistuu hieman lisää:
middle.setNeighbors("north" -> nForest, "east" -> tangle, "south" -> sForest, "west" -> clearing)
nForest.setNeighbors( "east" -> tangle, "south" -> middle, "west" -> clearing)
sForest.setNeighbors("north" -> middle, "east" -> tangle, "south" -> sForest, "west" -> clearing)
clearing.setNeighbors("north" -> nForest, "east" -> middle, "south" -> sForest, "west" -> nForest )
tangle.setNeighbors("north" -> nForest, "east" -> home, "south" -> sForest, "west" -> nForest )
home.setNeighbors( "west" -> tangle )
Samaa "tähtikikkaa" on muuten käytetty monen Scalan kirjastometodin
toteutuksessa. Esimerkiksi se, että voit luoda uuden hakurakenteen
käskyllä Map(...)
antaen kuinka monta parametria vain, perustuu
juuri tähän.
Neljäs refaktorointi: määritellään metodi datan luokse
Suuntien lisäämisen osalta käsittelemättä on vielä Adventure
-luokan
printAreaInfo
-metodi. Se näyttää alkuperäisessä ohjelmassa tältä:
def printAreaInfo() = {
val area = this.player.location
println("\n\n" + area.name)
println("-" * area.name.length)
println(area.description)
print("\nExits available:")
if (area.northNeighbor.isDefined) {
print(" north")
}
if (area.eastNeighbor.isDefined) {
print(" east")
}
if (area.southNeighbor.isDefined) {
print(" south")
}
if (area.westNeighbor.isDefined) {
print(" west")
}
println()
println()
}
(Sivumaininta: Yllä käytetty print
-metodi on samanlainen kuin println
, paitsi ettei
se vaihda riviä tulostettuaan merkit. Uloskäynnit siis tulostuvat yhdelle riville
peräkkäin.)
printAreaInfo
-metodin ongelmaksi olemme jo todenneet, että siinä luetellaan yksittäisiä
ilmansuuntia. Eikä ratkaisu enää edes toimi: uudessa versiossammehan naapureita kuvaavia
erillisiä ilmentymämuuttujia ei ole.
Lisäksi arveluttaa se, että tämä Adventure
-luokan metodi käsittelee lähes yksinomaan
sellaista dataa, joka on tallennettu toisaalle (Area
-olioon). Olio-ohjelmoinnissahan
ajatuksena olisi, että pyritään ensisijaisesti määrittelemään toiminnot niiden
käsittelemän datan yhteyteen. Näin toimimalla parannamme ohjelman koheesiota
(cohesion) eli kuhunkin ohjelmakomponenttiin kuuluvien osien keskinäistä
yhteenkuuluvuutta ja sitä kautta ohjelman luettavuutta ja ylläpidettävuuttä.
Mainitut ongelmat ratkeavat siirtämällä printAreaInfo
Adventure
-luokasta
Area
-luokkaan, jossa sen käsittelemä datakin on. Samalla voimme muokata sen
hyödyntämään neighbors
-ilmentymämuuttujaan tallennettua hakurakennetta:
def printAreaInfo() = {
println("\n\n" + this.name)
println(this.name.replaceAll(".", "-"))
println(this.description)
println("\nExits available: " + this.neighbors.keys.mkString(" ") + "\n\n")
}
Mitä olemme saaneet aikaan?
Yllä kuvattujen muutosten kautta tekstipelistä on saatu jatkokehityskelpoisempi. Samalla olet toivottavasti oppinut jotakin ohjelmien refaktoroinnista. Jotain on kuitenkin vielä tekemättä.
Luvun alussa mainittiin myös, että voisimme haluta tekstipelille omassa ikkunassaan toimivan graafisen käyttöliittymän. Tämä on nykyisellään vaikeaa, sillä AdventureDraft-toteutus rikkoo erästä tärkeää sovelluksen suunnittelun perusperiaatetta:
Käyttöliittymistä
Luvussa 1.2 ja 2.7 totesimme, että sovellusohjelmien pääosia ovat aihealueen malli ja käyttöliittymä. Tällainen jako on toteutettu useisiin kurssin varrella näkemiisi ohjelmiin.
Vaikka myös AdventureDraft-ohjelmassa on pakkaukset o1.adventure.draft.ui
(käyttöliittymä) ja o1.adventure.draft
(pelin sisäinen toiminta), ei tätä jakoa
ole toteutettu kunnolla. Nimittäin:
- Käyttöliittymä koostuu vain yhdestä käynnistysoliosta, joka
luo
Adventure
-olion ja kutsuu senrun
-metodia. Tämä olio ei huolehdi lainkaan käyttäjän kanssa kommunikoinnista eli tulostamisesta ja syötteen lukemisesta. - Toisaalta ohjelman sisäiseen malliin kuuluvat pelimaailmaa
mallintavat luokat tulostelevat ruudulle tietoja
milloin mistäkin metodista. Syötteen lukeminen tapahtuu
Adventure
-luokan metodissa, vaikka tämäkin luokka kuuluu ohjelman sisäiseen käsitemalliin.
Testauskin on huomioitava
Ohjelmoijat laativat varsinaisten käyttöliittymien ohelle testiohjelmia, joilla arvioivat, toimivatko malliin kuuluvat luokat tarkoituksenmukaisesti. Tällainen testiohjelma käyttää luokkia kattavasti ohjelmoijan määräämillä syötteillä kenestäkään varsinaisesta loppukäyttäjästä riippumattomasti.
(Järjestelmällinen automaattinen testaus on yleistä ja hyödyllistä, ja siihen palataan jatkokursseilla.)
Mitä jos haluamme korvata tekstikäyttöliittymän GUI-ikkunalla, joka näyttää pelin tilan tekstialuekomponenteissa kuten tämän luvun alkupään kuvassa? Muutoksia pitäisi tehdä sinne tänne ja korvata tulostus- ja syötteenlukukäskyjä GUI-koodilla. Ja entä jos haluamme tarjota samalle pelille kaksi vaihtoehtoista käyttöliittymää — graafinen ja tekstikonsolissa toimiva — joista käyttäjä voi valita kumman tahansa käynnistettäväksi? Pitäisikö tehdä kokonaan erilliset versiot pelistä, joissa iso osa koodista toistuu molemmissa?
Muotoillaan kysymys vielä yleisemmin: mitä jos halutaan käyttää ohjelman sisäistä mallia jossakin muussa käyttöliittymässä tai peräti toisessa sovelluksessa?
Mallin erottaminen käyttöliittymästä
Hyvin tehdyssä sovelluksessa sisäinen malli on mahdollisimman riippumaton käyttöliittymästä (ja mahdollisesta muusta mallia hyödyntävästä ohjelmakoodista kuten testiohjelmasta). Malliin kuuluvat ohjelman osat eivät kommunikoi käyttäjän kanssa. Malliin kuuluvista luokista ei esimerkiksi tulosteta näytölle, lueta näppäimistöä, avata ikkunoita tai reagoida nappulanpainalluksiin. Käyttöliittymää voi muuttaa tai vaihtaa koskematta malliin.
Lisätehtävä
Selvitä internetistä, millaista ohjelmointitapaa tarkoittaa Model–View–Controller eli MVC.
Entä miten o1
-kirjaston View
-luokka ja sen
konstruktoriparametri liittyvät asiaan?
Käyttöliittymä esittää mallin jollain tavoin ohjelman käyttäjälle (esim. tekstitulosteet pelimaailmasta) ja ottaa jollain tavoin käyttäjältä vastaan tietoja, joiden perusteella mallia käsitellään (esim. hahmonohjauskomennot). Se kutsuu malliin kuuluvien olioiden metodeita käyttäjän antamien syötteiden mukaisesti. Niinpä käyttöliittymän täytyy tietää tarkasti, millaista mallia sen kautta käytetään, eikä se voi olla mallista riippumaton.
Adventure-moduuli
Oheismoduuli Adventure on parempi pohja tekstipelin jatkokehitykselle kuin AdventureDraft. Adventure poikkeaa AdventureDraftistä näin:
- Siihen on tehty yllä esitetyt refaktoroinnit.
- Lisäksi malli on erotettu käyttöliittymästä:
- Kaikki tulostaminen ja näppäimistön lukeminen
tapahtuu luokassa
AdventureTextUI
. - Sisäiseen malliin kuuluvat luokat eivät tulosta mitään vaan vain palauttavat merkkijonoja, jotka kuvaavat alueita, komentojen seurauksia, tapahtumia jne. Käyttöliittymä valitsee, miten nämä merkkijonot esitetään käyttäjälle. (Oletuksena säilytämme, että mikä tahansa tämän pelin käyttöliittymä perustuu tavalla tai toisella tekstiin.)
- Kaikki tulostaminen ja näppäimistön lukeminen
tapahtuu luokassa
- Tarjolla on myös graafinen käyttöliittymä, jonka voi käynnistää
oliosta
AdventureGUI
. - Moduuli sisältää esineitä kuvaavan pikkuluokan
Item
, ja usean luokan Scaladoc-dokumentaatiossa on esineisiin liittyviä metodeita, joita ei vielä löydy koodista. Näiden merkitys selviää seuraavassa ohjelmointitehtävässä.
Tutustu nyt Adventure-ohjelmaan
Nyt on hyvä vaihe ottaa esiin Adventure-moduuli. Tutustu siihen itsenäisesti. Totea, miten se poikkeaa AdventureDraft-moduulista. Yksi tapa tutustua ohjelmaan on ajaa koodia debuggerissa rivi riviltä.
Älä unohda tutustua myös Action
-luokkaan, vaikka sitä ei tämän
luvun tekstissä olekaan suuremmin käsitelty.
Ohjelmointitehtävä: laajenna peliä
Täydennä moduulissa Adventure annettua metsäseikkailua esineillä. Siis:
- Alueessa (
Area
) pitää voida olla esineitä. Esineitä kuvaamaan on valmiina annettu luokka nimeltäItem
, mutta muut luokat eivät vielä hyödynnä sitä. - Alueen sisältämät esineet pitää tulostaa alueen muun kuvauksen yhteydessä, kun pelaaja on kyseisellä alueella.
- Pelaajan pitää pystyä poimimaan esineitä komennolla get itemname ja tiputtamaan niitä komennolla drop itemname.
- Pelaajan pitää pystyä tutkimaan poimimiaan esineitä komennolla examine itemname.
- Komennolla inventory pelaajan pitää pystyä "tekemään inventaario" esineistä, jotka hänellä on mukanaan.
- Pelin voittoehtoa pitää muuttaa hieman mutkikkaammaksi. Ei riitäkään, että pelaaja löytää tiensä kotiin, vaan pelaajan pitää ensin saada käsiinsä kaukosäädin ja paristoja ja vasta sitten päätyä kotiin.
- Hakurakenteet sopivat tässä hyvin esineiden tallentamiseen. Käytä niitä.
Alempana on esimerkki siitä, miten pelin pitäisi toimia, kun muutokset on tehty.
Ohjeita ja vinkkejä
- Annettuihin luokkiin täytyy lisätä uusia metodeita uusien toimintojen toteuttamiseksi. Näiden metodien kuvaukset löytyvät annetusta Scaladoc-dokumentaatiosta.
- Kaukosäätimen ja paristojen alkusijainnit on merkitty
Adventure
-luokan koodiin. - Pelin esineillä on yksiselitteiset nimet, eli kahdella esineellä ei voi olla samaa nimeä.
- Lisää ensin esineet pelin alueisiin. Työstä sen jälkeen komentoja, joilla esineitä voi käsitellä.
- Pidä huolta siitä, että ohjelman tuloste vastaa täsmälleen alla annettua esimerkkiä ja dokumentaatiota, tai A+ suuttuu.
Esimerkkituloste
You are lost in the woods. Find your way back home. Better hurry, 'cause Scalatut elämät is on real soon now. And you can't miss Scalkkarit, right? Forest ------ You are somewhere in the forest. There are a lot of trees here. Birds are singing. Exits available: west north south east Command: go east You go east. Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. Exits available: west north south east Command: go east You go east. Home ---- Home sweet home! Now the only thing you need is a working remote control. Exits available: west Command: inventory You are empty-handed. Home ---- Home sweet home! Now the only thing you need is a working remote control. Exits available: west Command: go east You can't go east. Home ---- Home sweet home! Now the only thing you need is a working remote control. Exits available: west Command: go west You go west. Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. Exits available: west north south east Command: get remote There is no remote here to pick up. Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. Exits available: west north south east Command: go west You go west. Forest ------ You are somewhere in the forest. A tangle of bushes blocks further passage north. Birds are singing. Exits available: west south east Command: go west You go west. Forest Clearing --------------- You are at a small clearing in the middle of forest. Nearly invisible, twisted paths lead in many directions. You see here: battery Exits available: west north south east Command: examine battery If you want to examine something, you need to pick it up first. Forest Clearing --------------- You are at a small clearing in the middle of forest. Nearly invisible, twisted paths lead in many directions. You see here: battery Exits available: west north south east Command: get battery You pick up the battery. Forest Clearing --------------- You are at a small clearing in the middle of forest. Nearly invisible, twisted paths lead in many directions. Exits available: west north south east Command: inventory You are carrying: battery Forest Clearing --------------- You are at a small clearing in the middle of forest. Nearly invisible, twisted paths lead in many directions. Exits available: west north south east Command: examine battery You look closely at the battery. It's a small battery cell. Looks new. Forest Clearing --------------- You are at a small clearing in the middle of forest. Nearly invisible, twisted paths lead in many directions. Exits available: west north south east Command: go south You go south. Forest ------ The forest just goes on and on. You see here: remote Exits available: west north south east Command: get remote You pick up the remote. Forest ------ The forest just goes on and on. Exits available: west north south east Command: examine remote You look closely at the remote. It's the remote control for your TV. What it was doing in the forest, you have no idea. Problem is, there's no battery. Forest ------ The forest just goes on and on. Exits available: west north south east Command: inventory You are carrying: remote battery Forest ------ The forest just goes on and on. Exits available: west north south east Command: go east You go east. Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. Exits available: west north south east Command: drop remot You don't have that! Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. Exits available: west north south east Command: drop remote You drop the remote. Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. You see here: remote Exits available: west north south east Command: drop remote You don't have that! Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. You see here: remote Exits available: west north south east Command: go east You go east. Home ---- Home sweet home! Now the only thing you need is a working remote control. Exits available: west Command: go west You go west. Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. You see here: remote Exits available: west north south east Command: get remot There is no remot here to pick up. Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. You see here: remote Exits available: west north south east Command: get remote You pick up the remote. Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. Exits available: west north south east Command: go east You go east. Home at last... and phew, just in time! Well done!
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Haastavanpuoleisia lisätehtäviä
Jos haluat ja ehdit, voit pohdiskella seuraavia.
- Yllä määriteltiin erikseen
Area
-luokkaan metoditneighbor
,setNeighbor
jasetNeighbors
, ja hakurakenneneighbors
oli yksityinen. Olisimme myös voineet tehdä hakurakenteesta julkisen. Tällöin mainittuja metodeita ei olisi välttämättä tarvittu. (Miksei?) Onko tällä ratkaisulla jotain haittapuolia? Erään perspektiivin aiheeseen voi antaa Demeterin laki. - Osaisitko refaktoroida
Action
-luokan niin, ettei siinä tarvitaif
/match
-käskyä ja toistuviaSome
-olioiden luomisia lainkaan? Vinkki: käytä hakurakennetta ja funktio-olioita. Onko ratkaisusi merkittävästi parempi tai huonompi kuin alkuperäinen? - Entä osaisitko refaktoroida tekstipelin niin, että
Action
-luokka olisikin abstrakti luokka tai piirreluokka, ja erilaisia toimintoja kuvaisivat sen alakäsitteiksi määritellyt konkreettiset luokat? MissäAction
-olioiden luomisen voisi tällöin hoitaa? Vinkki: käytä tehdasmetodia (luku 5.3). Onko ratkaisu merkittävästi parempi tai huonompi kuin alkuperäinen?
Näihin(kään) ei ole yhtä ainoaa oikeaa ratkaisua.
Yhteenvetoa
- Tapa, jolla ohjelma on laadittu, vaikuttaa siihen, kuinka helppoa ohjelmaan on tehdä lisäyksiä ja muita muutoksia.
- Ohjelmakoodin laatuominaisuuksia kuten muokattavuutta ja luettavuutta voidaan parantaa refaktoroimalla.
- Muokattavuus paranee usein vähentämällä ohjelman osien välisiä riippuvuuksia. Piilevät riippuvuudet ovat erityisen haitallisia.
- Lukuun liittyviä termejä sanastosivulla: refaktoroida, piilevä riippuvuus, abstraktio, koheesio, DRY, malli vs. käyttöliittymä.
Ryhdy jo miettimään omaa peliä!
Tekstipelin parissa jatketaan luvussa 10.1. Tuon luvun muodostaa yksi tehtävä, joka on niin iso, että koko kymppikierroksellekin on annettu aikaa kaksi viikkoa. Tässä lyhennetty versio tehtävänannosta: "Laadi ihan oma tekstipelisi muokkaamalla Adventure-ohjelmaa haluamallasi tavalla." Ideoinnin voit aloittaa vaikka heti. Aloita myös toteuttaminen heti kun voit; työlästä kymppikierrosta ei sovi jättää viimeiseen iltaan.
Palaute
Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.
Tekijät
Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!
Materiaalin luvut tehtävineen ja viikkokoosteineen on laatinut Juha Sorva.
Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.
Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee- ja Kelmu.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.
Tapa, jolla käytämme O1Libraryn työkaluja (kuten Pic
) yksinkertaiseen graafiseen
ohjelmointiin, on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi
oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.
Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki. Pääkehittäjänä on tällä hetkellä Markku Riekkinen, jonka lisäksi A+:aa ovat kehittäneet kymmenet Aallon opiskelijat ja muut.
A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen ovat luoneet Nikolai Denissov, Olli Kiljunen, Nikolas Drosdek, Styliani Tsovou, Jaakko Närhi ja Paweł Stróżański yhteistyössä Juha Sorvan, Otto Seppälän, Arto Hellaksen ja muiden kanssa.
Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.
neighbors
arvoksi hakurakenne, jossa avaimina on merkkijonoja (suuntien nimiä) ja arvoina alueolioita.