Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 2.6: Monenlaista käyttöä muuttujille
Tästä sivusta:
Pääkysymyksiä: Muuttujilla näköjään tehdään kaikenlaista? Miten laadin useasta oliosta koostuvan mallin kuvaamaan esimerkiksi graafisen pelin tilanteita? Miten viittaan laatimastani luokasta toiseen?
Mitä käsitellään? Muuttujia. Muuttujien rooleja eli käyttötapoja:
vakiot ja muut kiintoarvot, tilapäissäilöt, kokoojat, tuoreimman
säilyttäjät. Viittaukset ilmentymämuuttujissa; luokat
ilmentymämuuttujien tyyppeinä. Scalaan liittyviä yksityiskohtia:
tyhjät parametriluettelot, package
-määrittelyt.
Mitä tehdään? Ensin luetaan, sitten aloitetaan erään pelin ohjelmointi.
Suuntaa antava työläysarvio:? Pari tuntia. Alussa on helppo välipala. Lopun asiatkaan eivät ole erityisen haastavia esitietojen ollessa kunnossa.
Pistearvo: A55.
Muuttujien luokittelutapoja
Luku 1.4 kertoi: tietokoneohjelman muuttuja on nimetty varastointipaikka yhdelle arvolle. Tuo pätee kaikille muuttujille, jotka olet tähän mennessä kohdannut, mutta kohdatut muuttujat eroavat toisistaan monella muulla tavalla. On hyvä pysähtyä hetkeksi setvimään tilannetta.
Muuttujia voi ryhmitellä usealla eri perusteella.
Muutettavuuden mukaan
Scala-koodissa ilmeisimmällä tavalla näkyvä muuttujien luokittelu on jako var
- ja
val
-muuttujiin, jota käsiteltiin luvussa 1.4. Sen voi esittää kuvallisesti vaikkapa
näin:
Tätä jakoa ei ole tässä muodossa läheskään kaikissa muissa ohjelmointikielissä.
Tietotyypin mukaan
Toinen melko ilmeinen tapa on jakaa muuttujat tietotyypin mukaan:
Käyttöyhteyden mukaan
Kolmas jaottelu voidaan tehdä sen mukaan, mihin yhteyteen muuttuja on määritelty.
Osa muuttujista on ilmentymämuuttujia olioiden yhteydessä (luku 2.4). Osa on paikallisia muuttujia, jotka ovat väliaikaisesti olemassa kutsupinon kehyksissä tietyn aliohjelmakutsun ajan. Erikoistapauksen paikallisista muuttujista muodostavat parametrimuuttujat, joihin ei sijoiteta arvoa sijoituskäskyllä vaan jotka saavat arvonsa aliohjelmakutsun parametrilausekkeet evaluoimalla (luku 1.7). Myös REPLissä määrittelemiemme muuttujien voidaan sanoa olevan eräänlaisia paikallisia muuttujia, joiden elinkaari on REPL-käyttökerran mittainen.
Muuttujien roolit
Muuttujia voi ryhmitellä myös niiden käyttötavan eli roolin mukaan. Vaikka tästä ei vielä ole nimenomaisesti puhetta ollutkaan, niin olet jo nähnyt muuttujien käyttöä muutamassa eri roolissa. Perehdytään asiaan tarkemmin:
Tilioliollamme (luku 2.2) oli pari muuttujaa, joita ei käytetty keskenään samaan tapaan.
Tilin numero
-muuttujan arvo pysyi aina samana. Toisaalta tilin saldo
-muuttujan
arvo vaihtui talletusten ja nostojen yhteydessä: uusi saldo määrittyi vanhasta saldosta
ja muutoksen koosta. Näillä muuttujilla oli siis eri roolit.
Muuttujan rooli (role) on kuvaus siitä, miten tiettyä muuttujaa käytetään
ohjelmassa: millä perusteella sen arvoa vaihdetaan, jos sitä yleensäkään vaihdetaan?
Aiheesta tehtyjen tutkimusten perusteella on todettu, että valtaosaa tietokoneohjelmissa
esiintyvistä muuttujista kuvaa osuvasti yksi noin kymmenestä roolinimikkeestä. Kahdeksan
roolinimeä riittää luonnehtimaan lähes kaikkia tämän kurssin esimerkkiohjelmien muuttujia.
Voimme esimerkiksi sanoa, että tilioliomme saldo
on rooliltaan "kokooja": sen kulloinenkin
arvo on saatu kokoamalla tehtyjen toimenpiteiden yhteisvaikutus, siis talletusten ja
nostojen summa.
Muuttujien rooleja käytetään jatkossa tässä kurssimateriaalissa selkiyttämään ohjelmien suunnittelua. Monissa kurssin esimerkkimoduuleissa on rooleja myös merkitty ohjelmakoodin kommentteihin alla näkyvään tapaan.
var saldo = 0.0 // kokooja
On tärkeää huomata, ettei muuttujan rooli ole määritelmä siitä, mitä muuttujalla voisi
periaatteessa tehdä. Esimerkiksi saldo
-muuttujaanhan voisi teknisesti ottaen sijoittaa
aivan mitä vain lukuarvoja, jos ohjelmoija niin määrää. Rooli on kuvaus siitä, mitä
jollakin muuttujalla todella tehdään tietyssä ohjelmassa. Roolilla on merkitystä
ihmiselle, ei tietokoneelle.
Käydään seuraavaksi läpi ne muutama rooli, joista kurssimateriaalissa on jo ollut kunnollisia esimerkkejä.
Kiintoarvot
Yksinkertaisin muuttujan rooli on kiintoarvo (fixed value). Sen jälkeen,
kun kiintoarvoisen muuttujan arvo on kerran alustettu, sitä ei enää vaihdeta
lainkaan. Kiintoarvot ovat Scalassa lähes poikkeuksetta val
-muuttujia.
Ilmentymämuuttuja, joka on kiintoarvo, kuvaa jotakin olion pysyvää ominaisuutta (esim. tilinumero).
Tyypillisiä esimerkkejä paikallisista kiintoarvomuuttujista ovat parametrimuuttujat.
Vaikkapa Tyontekija
-luokan kuukausikulut
-metodin kulukerroin
-parametrimuuttuja
on kiintoarvo, joka on koko metodikutsun ajan muuttumaton:
def kuukausikulut(kulukerroin: Double) = this.kkpalkka * this.tyoaika * kulukerroin
Vakiot
Sellaisia kiintoarvoisia muuttujia, joiden arvo on tiedossa jo ennen ohjelman ajoa, sanotaan usein vakioiksi (constant). Vakioksi voidaan kirjata jokin yleismaailmallinen totuus kuten piin likiarvo tai jokin tiettyyn sovellukseen liittyvä muuttumaton ja etukäteen tiedossa oleva asia.
val Pi = 3.141592653589793
val MinimumAge = 18
val DefaultGreeting = "Hello!"
Vakioiden avulla voi usein kätevästi parantaa ohjelmien luettavuutta. Vakion nimi koodissa kertoo "maagista arvoa" eli ohjelman toimintaan dokumentoimattomalla tavalla liittyvää literaalia paremmin, mitä koodi tekee.
Muokattavuuskin voi parantua. Jos vakioarvo halutaan myöhemmin vaihtaa toiseksi, kun ohjelmasta laaditaan uusittu versio, niin tämä onnistuu yhdestä paikasta muuttamalla vakion määrittelyä, vaikka vakiota olisikin käytetty eri puolilla ohjelmaa (tai eri ohjelmissa). Maagisten arvojen käyttö useassa paikassa aiheuttaa ohjelmakoodin osien välille implisiittisiä riippuvuuksia (implicit coupling): yhtä kohtaa muutettaessa pitäisi aina muistaa muuttaa toisiakin sieltä täältä. Implisiittiset riippuvuudet aiheuttavat koodiin helposti bugeja.
Vakioita löytyy myös ohjelmakirjastoista. Esimerkiksi pakkauksessa scala.math
on
määritelty kiintoarvoinen muuttuja nimeltä Pi
, johon on tallennettu piin likiarvo.
Pakkauksen o1
tarjoamat värit (Red
, Blue
, CornflowerBlue
jne.) ovat vakioita,
joista kukin viittaa tiettyyn Color
-tyyppiseen olioon.
Tilapäissäilöt
Lukujen 1.8 ja 2.2 tehtävissä olet itsekin toteuttanut tilapäissäilöjä (temporary; ohjelmoijien suussa usein "temppi" tms.). Tilapäissäilö on muuttuja, johon otetaan väliaikaisesti talteen jokin arvo, jota tarvitaan algoritmin myöhemmissä vaiheissa. Esimerkiksi tiliolion nostometodissa täytyi ottaa nostettava summa tilapäisesti talteen saldon laskemisen ajaksi, jonka jälkeen se palautettiin.
def nosta(summa: Int) = {
val nostettu = min(summa, this.saldo) // nostettu on tilapäissäilö
this.saldo = this.saldo - nostettu
nostettu
}
Tilapäissäilöön voi myös esimerkiksi ottaa välituloksen talteen aritmeettisessa laskennassa. Tyypillisimmin tilapäissäilöt ovat paikallisia muuttujia.
Tilapäissäilön arvo ei yleensä vaihdu, kun se on kerran asetettu; Scala-ohjelmien
tilapäissäilöt ovat melkein aina val
-muuttujia. Joissakin tapauksissa on makuasia,
kutsuuko muuttujaa kiintoarvoksi vai tilapäissäilöksi.
Kokoojat
Kun kokoojalle (gatherer) sijoitetaan uusi arvo, se määritetään yhdistämällä jollakin tapaa kokoojan vanha arvo ja jokin uusi data (esim. käyttäjän antama syöte tai muu parametriarvo).
Esimerkkejä ilmentymämuuttujista, jotka ovat kokoojia:
- Tilin saldo (selitetty yllä). Käsky
this.saldo = this.saldo - nostettu
on kokoojille tyypillinen: siinä uusi saldo lasketaan vanhan saldon ja nostetun määrän perusteella. - Saldoa vastaavasti toimivat hirviön kuntopisteet (luku 2.4).
- Pelihahmon sijainti pelissä, jossa uusi sijainti määräytyy
aina vanhan sijainnin ja annetun kulkusuunnan perusteella.
Nykysijainti siis riippuu kaikista hahmolle aiemmin annetuista
liikkumiskomennoista.
- Koodi voi näyttää esimerkiksi tältä:
this.sijainti = this.sijainti.naapuri(kulkusuunta)
. (Tässä hahmo-olio kysyy itseensä liitetyltä paikkaa kuvaavalta oliolta, mikä sen naapuri tietyssä kulkusuunnassa on, ja asettaa uudeksi sijainniksi saamansa vastauksen.) - Jotain samantapaista on luvassa myös alempana tässä luvussa.
- Koodi voi näyttää esimerkiksi tältä:
Voidaan ajatella, että kokooja ikään kuin "ottaa sisään juttuja" ja sen arvo riippuu kaikista "aiemmin käsitellyistä jutuista".
Kokoojat ovat myös paikallisina muuttujina yleisiä. Ensimmäisen esimerkin tästä näet luvussa 5.5.
Tuoreimman säilyttäjät
Tyontekija
-olion nimeksi voi sijoittaa uuden arvon:
val testi = new Tyontekija("Leena Herppeenluoma", 1957, 7000)testi: o1.Tyontekija = o1.Tyontekija@1100b25 testi.nimires0: String = Leena Herppeenluoma testi.nimi = "Leena Hefner"testi.nimi: String = Leena Hefner
Muuttujaa nimi
käytetään pitämään kirjaa viimeisimmästä työntekijäoliolle asetetusta
nimestä. Toisin kuin kokoojan tapauksessa, aiempi nimi ei ole osatekijänä uutta nimeä
muodostettaessa, vaan uusi annettu arvo yksinkertaisesti korvaa aiemman. Muuttuja
kuten nimi
, joka pitää kirjaa viimeisimmästä tietynlaisesta arvosta, on rooliltaan
tuoreimman säilyttäjä (most-recent holder).
Äskeinen esimerkki on tyypillinen tapaus: ilmentymämuuttujaa käytetään tuoreimman säilyttäjänä kuvaamassa sellaista olion ominaisuutta, jonka arvoa voi vaihtaa. Tuoreimman säilyttäjille löytyy käyttöä myös paikallisina muuttujina, mistä näet esimerkkejä mm. luvussa 5.5.
Roolien merkityksestä
Roolit kuvaavat tyypillisiä asioita, joita muuttujilla ohjelmissa tehdään. Ne tarjoavat työkalupakin, jota voi käyttää apuna ohjelmoidessa ja ohjelmia lukiessa. Jos tiedät tietyn muuttujan roolin, tiedät myös jotain siitä, miten ohjelma toimii.
Kukin rooli kuvaa abstraktin ratkaisun sellaiseen tarpeeseen, joka toistuu kerta toisensa jälkeen mitä erilaisimpien ohjelmointiongelmien osana. Käyttämämme roolinimikkeet on valittu vastaamaan sellaisia yhtäläisyyksiä muuttujien käytössä, joita kokeneet ohjelmoijat näkevät toisistaan muuten riippumattomienkin ohjelmien välillä.
Rooleista ja suunnittelumalleista
Roolit ovat eräs tapa nimetä ohjelmissa usein esiintyviä ratkaisumalleja. Ne liittyvät yksittäisiin muuttujiin eli hyvin pieniin ohjelman osiin. Myös suuremmille ratkaisumalleille on kehitetty nimiä ja kuvauksia. Tunnetuin esimerkki tästä ovat suunnittelumallit (design patterns), jotka kuvaavat ohjelmakokonaisuuden suunnittelussa usein toistuvia tilanteita ja niille sopivia yleensä yhden tai useamman kokonaisen luokan suuruisia ratkaisuja. Suunnittelumalleja käsitellään mm. kurssilla CS-C2120 Ohjelmointistudio 2.
Toisin kuin jotkin suunnittelumallit, roolinimikkeet eivät kuulu useimpien ammattiohjelmoijien sanastoon, mutta saattaa roolinimistä olla hyötyä heillekin esimerkiksi koodin dokumentoinnissa. Ehkä seuraava ohjelmoijasukupolvi tuntee ne vähän paremmin?
Sinun opiskelijana ei ole pakko itse käyttää roolinimiä. Kuitenkin roolien käytöstä voi olla sinullekin apua hahmotellessasi ratkaisuja ohjelmointiongelmiin; moni aloitteleva ohjelmoija on niistä hyötynyt. Yksi ohjelmoinnin oppimisen haasteista on oppia tunnistamaan yleisiä ohjelmointiongelmissa esiintyviä tarpeita ja tilanteita sekä niihin sopivia ratkaisuja. Roolit voivat osaltaan auttaa tässä. Ne voivat antaa aloittelevalle ohjelmoijalle vinkkiä siitä, mihin muuttujia voi käyttää, ja siitä, mitä kannattaa tehdä, kun halutaan saavuttaa tietynlainen tavoite.
Voit hyödyntää rooleja ajattelun tukena:
"Hmm... Pitäisi laskea syötettyjen mittaustulosten summa... Summastahan voisi pitää kirjaa kokoojamuuttujassa, jonka arvoa päivitetään aina kun käsitellään uusi mittaustulos: lasketaan vanhan summan ja uuden mittaustuloksen summa."
Kun suunnittelet ohjelmia, mieti sekä tarvitsemiesi muuttujien tietotyypit että roolit. Kun luet ohjelmia, kiinnitä huomiota muuttujien käyttötapoihin. Kun dokumentoit ohjelmia, roolien merkitseminen koodiin saattaa auttaa lukijaa.
Toisiinsa viittaavia olioita
Aiemmista esimerkeistä on käynyt ilmi, että olio-ohjelman toiminta perustuu olioiden
väliseen viestintään. Viestinnän pohjana ovat olioiden välille muodostetut yhteydet:
esimerkiksi kurssioliosta voi olla viittaus kurssin opetuspaikkaa kuvaavaan saliolioon
sekä ilmoittautuneita opiskelijoita kuvaaviin olioihin, ja GoodStuff-moduuliin
Category
-olioon voi liittyä useita Experience
-olioita. Pelihahmoa kuvaavan olion
tietoihin voi kuulua viittaus sijaintiolioon.
Aiemmissa luvuissa olet nähnyt, miten tietynlaisten olioiden ominaisuudet kuvataan luokkana. Kuitenkaan vastaan ei ole vielä tullut sellaista koodiesimerkkiä, jossa yhdestä itse laaditusta luokasta viitattaisiin toiseen. Nyt tulee.
Käsitellään ensin irrallista esimerkkiä, jonka kaksi luokkaa kuvaavat — erittäin yksinkertaistetusti! — tilauksia ja asiakkaita kuvitteellisessa verkkokauppaohjelmassa. Sen jälkeen pääset soveltamaan esimerkin oppeja, kun laadit oliomallin peliä varten.
Tavoite: luokat asiakkaille ja tilauksille
Asiakasoliota luodessa annetaan konstruktoriparametreiksi nimi, asiakasnumero sekä sähköposti- ja katupostiosoitteet:
val testihenkilo = new Asiakas("T. Testaaja", 12345, "test@test.fi", "Testitie 1, 00100 Testaamo")testihenkilo: o1.Asiakas = o1.Asiakas@a7de1d
Asiakasoliolta voi pyytää tekstimuotoisen kuvauksen parametrittomalla kuvaus
-metodilla:
println(testihenkilo.kuvaus)#12345 T. Testaaja <test@test.fi>
Tilausoliolle annetaan konstruktoriparametreiksi tilausnumero ja tilaaja. Jälkimmäinen näistä on viittaus asiakasolioon:
val testitilaus = new Tilaus(10001, testihenkilo)testitilaus: o1.Tilaus = o1.Tilaus@18c6974
Tilaukseen voi lisätä tuotteita lisaaTuote
-metodilla. Tässä esimerkissä yksittäisistä
tuotteista ei varsinaisesti tallenneta tietoja, vaan ainoastaan ilmoitetaan tuotteen
kappalehinta ja montako kappaletta tilataan.
Tässä tilataan 100 kpl 10,50 euron tuotteita ja yksi 500 euron tuote:
testitilaus.lisaaTuote(10.5, 100)testitilaus.lisaaTuote(500.0, 1)
Myös tilausolioilla on kuvaus
-niminen metodi:
println(testitilaus.kuvaus)tilaus 10001, tilaaja: #12345 T. Testaaja <test@test.fi>, yhteensä 1550.0 euroa
Kuten yltä näkyy, tilaajan kuvaus muodostaa osan tilausta kuvaavasta merkkijonosta.
Tilausoliolta voi myös kysyä, mikä asiakasolio on tilaajana, jolloin saadaan viittaus juuri siihen asiakasolioon, joka tilaajaksi aiemmin asetettiin:
testitilaus.tilaajares1: Asiakas = o1.Asiakas@a7de1d
Huomaa, että saimme nimenomaan Asiakas
-tyyppisen viittauksen, emme merkkijonoa.
Pääsimme tilausolion kautta käsiksi tilaukseen liittyvään toiseen olioon, ja voimme
käyttää tuota oliota tavalliseen tapaan, vaikkapa näin ketjuttaen:
testitilaus.tilaaja.osoiteres2: String = Testitie 1, 00100 Testaamo
Tässä siis testitilaus
-muuttuja sisältää viittauksen tilausolioon, tuon tilausolion
tilaaja
-muuttuja viittauksen asiakasolioon, ja asiakasolion osoite
-ilmentymämuuttuja
viittauksen merkkijonoon.
Luokkien toteutus
Tässä ensin asiakasluokka. Siinä ei ole mitään uudenlaista:
class Asiakas(val nimi: String, val asiakasnumero: Int, val email: String, val osoite: String) {
def kuvaus = "#" + this.asiakasnumero + " " + this.nimi + " <" + this.email + ">"
}
Hahmotellaan tilauksia kuvaava luokka ensin pseudokoodina (luku 2.5):
class Tilaus(kiintoarvoisia tietoja: tilausnumero ja tilauksen tehnyt asiakas) { kokoojamuuttuja pitäköön kirjaa kokonaishinnasta, joka on aluksi nolla def lisaaTuote(kappalehinta: Double, lukumaara: Int) = { lisää kokonaishinnan kokoojaan parametrien mukainen summa } def kuvaus = palauta merkkijonokuvaus tilauksen tiedoista; pyydä siihen sisältyvät asiakastiedot kyseiseltä asiakasoliolta }
Pseudokoodin kuvaama ajatus on helppo esittää myös varsinaisena ohjelmakoodina, sillä
itse laadittuakin luokkaa kuten Asiakas
voi mainiosti käyttää toisen luokan
määrittelyssä:
class Tilaus(val numero: Int, val tilaaja: Asiakas) {
var kokonaishinta = 0.0
def lisaaTuote(kappalehinta: Double, lukumaara: Int) = {
this.kokonaishinta = this.kokonaishinta + kappalehinta * lukumaara
}
def kuvaus = "tilaus " + this.numero + ", " +
"tilaaja: " + this.tilaaja.kuvaus + ", " +
"yhteensä " + this.kokonaishinta + " euroa"
}
Asiakas
-luokka on Tilaus
-luokan konstruktoriparametrin
tyyppinä, joten tilausoliota luotaessa on jälkimmäiseksi
parametriksi annettava viittaus asiakasolioon. val
-sanalla
ilmoitetaan, että kyseinen viittaus halutaan talteen ilmentymämuuttujaan.this.tilaaja
arvo on viittaus asiakasolioon, jonka
metodia voi kutsua kirjoittamalla this.tilaaja.kuvaus
. Tässä
siis tilausolio pyytää asiakasoliota kertomaan kuvauksensa ja
käyttää sitten vastauksena saamaansa merkkijonoa osana omaa
kuvaustaan.Näin saimme määriteltyä suhteen tilausluokan ja asiakasluokan välille ja sitä kautta myös suhteen kustakin tilausoliosta yhteen asiakasolioon. Olioiden välisiä suhteita on kuvattu myös seuraavassa diagrammissa.
Lisätehtävä: toString
-metodeita
Vapaaehtoisena harjoituksena voit muokata annettuja Asiakas
- ja
Tilaus
-luokkia siten, että niillä on kuvaus
-metodin sijaan
toString
-metodi (luku 2.5). Luokat löytyvät Oliointro
-moduulista.
Kunhan toteutat asiakkaiden toString
in, niin Tilaus
-luokan
toString
-metodissa sinun ei ole pakko nimenomaisesti kutsua
sitä. Riittää yhdistää toisiinsa merkkijono ja viittaus asiakasolioon.
Jos haluat, voit myös muokata luokkien toString
-metodit käyttämään
merkkijonoupotuksia (s
ja dollarit) plussien sijaan.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Entä suhteet yhdestä moneen olioon?
Entä jos haluamme viitata yhdestä kurssista useaan ilmoittautujaan tai yhdestä kategoriasta useaan kokemukseen? Tai vaikka kirjata kullekin asiakkaalle luettelo hänen tilauksistaan?
Noiden kysymysten vastaukseen pääsemme kunnolla käsiksi luvussa 4.2. Lyhyesti sanottuna se on: kytketään olioon kokoelma, jonka alkioina ovat nuo ilmoittautujat, kokemukset tai tilaukset.
Lisää esimerkkejä
Esimerkki: aarteiden vartijoita
Luvussa 2.4 hahmottelimme luokkaa Hirvio
. Tässä siitä toimiva versio (jossa
kuvaus
-metodi on korvattu toString
-metodilla):
class Hirvio(val tyyppi: String, val taysiKunto: Int) {
var nykykunto = taysiKunto
override def toString = this.tyyppi + " (" + this.nykykunto + "/" + this.taysiKunto + ")"
def vahingoitu(paljonko: Int) = {
this.nykykunto = this.nykykunto - paljonko
}
}
Vaikka emme tätä esimerkkiä oikeaksi peliksi työstäkään, jatketaan kuitenkin tätä teemaa yhdellä pikkuluokalla.
Sanotaan, että kuvitteellisessa pelissämme on eriarvoisia aarteita, joita kutakin
vartioi peikko — jokaiseen Aarre
-olioon liittyy sen vartijana toimiva Hirvio
-olio.
Tässä ensin käyttöesimerkki:
val kulta = new Aarre(1000.0, 50)kulta: Aarre = aarre (arvo 1000.0), jota vartioi peikko (50/50) kulta.arvores3: Double = 1000.0 kulta.vartijares4: Hirvio = peikko (50/50) kulta.houkuttelevuusres5: Double = 20.0
Alla on toteutus Aarre
-luokalle.
class Aarre(val arvo: Double, val haaste: Int) {
def vartija = new Hirvio("peikko", this.haaste)
def houkuttelevuus = this.arvo / this.vartija.nykykunto
override def toString = "aarre (arvo " + this.arvo + "), jota vartioi " + this.vartija
}
Esimerkki: pomojen pomoja
Esimerkki: taivaankappaleita
Tämä on valinnainen lisäesimerkki yhden luokan käytöstä toisen määrittelyssä ja olioiden
liittämisestä näin toisiinsa. Voit käydä esimerkin läpi, jos Tilaus
- ja Aarre
-esimerkit
jäivät epäselviksi tai jos ajaudut hankaluuksiin seuraavana alla tulevan pelihankkeen
kanssa. Uutta asiaa tässä esimerkissä ei ole.
Luokat Taivaankappale
ja Avaruus
Laaditaan kokeeksi pari luokkaa, joilla mallinnamme Maan ja Kuun
sijaintia avaruudessa kaksiulotteisessa koordinaatistossa.
Taivaankappale
-luokan ilmentymät vastaavat yksittäisiä
taivaankappaleita. Avaruus
-luokan ilmentymä puolestaan on osio
avaruudesta, jossa Maa ja Kuu sijaitsevat: kuhunkin Avaruus
-olioon
liittyy pari Taivaankappale
-oliota. (Vertaa: kuhunkin Tilaus
-olioon
liittyy yksi Asiakas
; kuhunkin Aarre
-olioon liittyy yksi Hirvio
.)
Tässä ensin yksinkertainen luokka Taivaankappale
.
class Taivaankappale(val nimi: String, val sade: Double, var sijainti: Pos) {
def halkaisija = this.sade * 2
override def toString = this.nimi
}
Pos
-olio: kunkin taivaankappaleolion
osaksi tallennetaan viittaus Pos
-olioon, joka
pitää sisällään x
- ja y
-koordinaatin.Tässä Taivaankappale
-luokkaa käyttävä Avaruus
-luokka:
class Avaruus(koko: Int) {
val leveys = koko * 2
val korkeus = koko
val maa = new Taivaankappale("Maa", 15.9, new Pos(10, this.korkeus / 2))
val kuu = new Taivaankappale("Kuu", 4.3, new Pos(971, this.korkeus / 2))
override def toString = s"${this.leveys}x${this.korkeus} alue avaruudessa"
}
Avaruus
-oliota luotaessa ilmaistaan konstrutoriparametrilla
sen koko (koordinaattiyksiköissä). Sovitaan tässä pikkuesimerkissä,
että tällainen avaruuden kaistale on aina kaksi kertaa niin leveä
kuin korkea. Leveys ja korkeus tallennetaan Avaruus
-olion
ilmentymämuuttujiin.Taivaankappale
-oliosta. Leluesimerkissämme niillä on juuri
tietyt nimet, säteet ja sijainnit, jotka kirjaamme koodiin suoraan.Noin määriteltyjä luokkia voi käyttää REPLissä näin:
val avaruus = new Avaruus(500)avaruus: Avaruus = 1000x500 alue avaruudessa avaruus.maares6: Taivaankappale = Maa avaruus.kuures7: Taivaankappale = Kuu
Avaruus
-olion, jolloin...Taivaankappale
-oliota, joihin
Avaruus
-olion ilmentymämuuttujat maa
ja kuu
viittaavat.Voimme käyttää näiden olioiden muuttujia ja metodeita:
avaruus.maa.halkaisijares8: Double = 31.8 avaruus.maa.sijaintires9: o1.Pos = (10.0,250.0) avaruus.kuu.sijaintires10: o1.Pos = (971.0,250.0) avaruus.kuu.saderes11: Double = 4.3 avaruus.kuu.sijainti.xDiff(avaruus.maa.sijainti)res12: Double = -961.0
Taivaankappale
-oliota, joihin pääsemme käsiksi
muuttujien kautta.Taivaankappale
-olioihin on puolestaan kytketty
Pos
-oliot, joihin pääsee käsiksi taivaankappaleiden
sijainti
-muuttujista.Tämä valinnainen esimerkki saa jatkoa seuraavassa luvussa 2.7, jossa piirrämme tuollaista mallia vastaavan näkymän avaruudesta.
Peliprojekti
Aloittakaamme uusi hanke, jota kehität vähitellen useassa luvussa samalla kun perehdyt syvemmin GoodStuff-kestoprojektiin ja teet muita erillisharjoituksia. Tuloksena syntyy konkreettinen peliohjelma.
FlappyBug
Tehdään ohjelma, jossa pelaaja ohjaa leppäkerttua eli "ötökkää" ja väistelee esteitä.
Ötökkä liikahtaa ylös käyttäjän komentaessa sitä iskemään siivillään. Toisaalta ötökkä vajoaa koko ajan alaspäin, joten käyttäjän on pidettävä se ilmassa komennoillaan. Ötökkä liikkuu vain pystysuoraan; esteitä lentää oikealta vaakasuoraan.
Tässä luvussa aloitamme laatimaan mallin ohjelman sisuskaluista, emme käyttöliittymää. Tässä tapauksessa malli tarkoittaa pelin sisältöön liittyviä käsitteitä (esim. este) ja niihin liittyviä toimintoja (esim. lentäminen).
Aloitamme yksinkertaistetusta pelistä, jossa on yksi ötökkä ja vain yksi este. Tulevissa luvuissa paitsi kehitämme nyt laadittavaa mallia monipuolisemmaksi myös laadimme pelille käyttöliittymän.
Laaditaan seuraavat luokat:
Bug
: kuvaa otökän käsitettä, sen ominaisuuksia ja toimintoja.Obstacle
: kuvaa esteitä vastaavasti.Game
: Tämän luokan ilmentymä vastaa yhtä pelikertaa ja pitää kirjaa sen etenemisestä.Game
-olion kautta pääsee käsiksi pelin osiin (ötökkään ja esteeseen), ja se määrää, miten noiden osien toimintoja pelin edetessä aktivoidaan.
Aloitetaan luokasta Obstacle
, jolle syntyy toteutus alla olevassa esimerkissä. Sitten
pääset itse laatimaan Bug
in ja Game
n harjoitustehtävissä.
Luokka Obstacle
Obstacle
-oliota luodessa määritetään sen koko (säde) sekä lähtösijainti pelialueen
kattavassa kaksiulotteisessa koordinaatiossa. Esimerkiksi näin:
val bigObstacle = new Obstacle(150, new Pos(800, 200))bigObstacle: o1.Obstacle = center at (800.0,200.0), radius 150
Yksinkertaisessa peliversiossamme este ei juuri tee mitään, mutta lentää se osaa:
bigObstacle.approach()bigObstacleres13: o1.Obstacle = center at (790.0,200.0), radius 150 bigObstacle.approach()bigObstacle.approach()bigObstacleres14: o1.Obstacle = center at (770.0,200.0), radius 150
Obstacle
-oliolla on muuttuva tila, johon approach
-metodin
kutsuminen vaikuttaa. Aina, kun metodia kutsutaan, esteen
x-koordinaatti vähenee kymmenellä.Pseudokoodi toteutuksesta:
class Obstacle(kiintoarvoinen muuttuja säteelle, kokooja sijainnille) { def approach() = { Sijoita sijainnista kirjaa pitävälle kokoojalleni uusi arvo. Uusi arvo saadaan vanhasta lisäämällä x-koordinaattiin -10. } override def toString = yhdistä esteen tiedot esimerkin mukaiseksi merkkijonoksi }
Scalaksi:
class Obstacle(val radius: Int, var pos: Pos) {
def approach() = {
this.pos = this.pos.addX(-10)
}
override def toString = "center at " + this.pos + ", radius " + this.radius
}
Ennen kuin etenemme luokkien Bug
ja Game
pariin, kiinnitä huomio kahteen asiaan:
approach
-metodin määrittelyssä on tyhjät
kaarisulkeet. Miksi? Ehkä ihmettelit samaa jo ylempänä, missä
approach
-metodia kutsuttaessa käytettiin vastaavasti tyhjiä
sulkeita.Maaginen luku vakioksi
Määritellään maagisen luvun syrjäyttäjäksi vakio:
val ObstacleSpeed = 10
Tehdään nyt niin, että määrittelemme tällaisia FlappyBug-pelin sääntöihin liittyviä vakioita varten erillisen paikan.
Oheismoduulista FlappyBug löydät osittaisen toteutuksen pelille, jossa tuo vakio
on määritelty tiedostoon constants.scala
. Esteluokka on puolestaan tiedostossa
Obstacle.scala
, ja sen approach
-metodi hyödyntää vakiota:
import constants._
class Obstacle(val radius: Int, var pos: Pos) {
def approach() = {
this.pos = this.pos.addX(-ObstacleSpeed)
}
override def toString = "center at " + this.pos + ", radius " + this.radius
}
constants
-yksittäisoliossa.
Otamme ne käyttöön.Välipuhe parametrittomista vaikutuksellisista metodeista — ja sulkeista
Äskeisessä koodissa esiintyneet tyhjät sulkeet liittyvät siihen, että kyseessä on
parametriton metodi: parametriluettelo on tyhjä. Toisaalta olemme laatineet sellaisia
parametrittomia metodeita, joihin ei ole tyhjiä sulkeita kirjoitettu. Tällaisiahan ovat
äskeinen toString
ja kuvaus
ylempänä ja moni muu aiempien esimerkkien metodi.
Kyseessä on Scala-kielen käytäntö, jolla vaikutukselliset ja vaikutuksettomat parametrittomat
metodit erotetaan toisistaan. Vaikutuksellisiin parametrittomiin metodeihin (kuten
approach
) kirjoitetaan tyhjät kaarisulkeet parametrittomuuden merkiksi. Toisaalta
vaikutuksettomista parametrittomista metodeista (kuten toString
tai kuvaus
) nämä
sulkeet jätetään pois. Tämä siis lisäyksenä funktioiden määrittelemiseen liittyviin
välimerkkisääntöihin luvusta 1.7.
Tyhjiä kaarisulkeita käytetään tai ollaan käyttämättä vastaavasti myös metodeita
kutsuttaessa. Sulkeet metodikutsussa bigObstacle.approach()
korostavat ohjelmakoodin
lukijalle, että kyseessä on metodikutsu, joka vaikuttaa esteolion tilaan.
Yhtenäinen osoitusperiaate
Ohessa mainitun tiettyjen metodikutsujen sulkeettomuuden taustalla on yhtenäisen osoituksen periaate (uniform access principle), josta voit lukea lisää Wikipediassa.
Toisaalta on niin, että lausekkeesta testihenkilo.kuvaus
ei näy, onko kyseessä
kuvaus
-nimisen metodin kutsu vai kuvaus
-nimisen muuttujan arvon tutkiminen.
Scala-kielen kehittäjät ovat nimenomaan halunneet, että vaikutuksettoman parametrittoman
metodin kutsu näyttää ohjelmakoodissa täsmälleen samalta kuin ilmentymämuuttujan arvon
katsominen (joka ei sekään muuta ohjelman tilaa). Tälle halulle on syynsä, joista tässä
vaiheessa ymmärrettävin on se, että luokan käyttäminen on näin hieman helpompaa: luokan
käyttäjän ei tarvitse muistella, onko kyseessä ilmentymämuuttuja vai metodi.
Noudata näitä suljesääntöjä
Äsken esiteltyjä sulkeidenkäyttösääntöjä tulee noudattaa. Erityisesti: kun teet tehtävää, jossa tehtävänannossa on tyhjät sulkeet metodin nimen perässä, niin laita sulkeet myös laatimaasi toteuttavaan ohjelmakoodiin ja käytä sulkeita tuota metodia kutsuessasi.
FlappyBug-tehtävä (osa 1/17: Bug
-luokka)
Ötökän toivottu toimintatapa
Näin luodaan ötökkäolio:
val myBug = new Bug(new Pos(300, 200))myBug: o1.Bug = center at (300.0,200.0), radius 15
Kuten toString
in ylle tuottamasta kuvauksestakin voi päätellä, ötököillä on esteiden
tapaan sijainti ja säde. Niitä voi tutkia myös erikseen:
myBug.posres15: o1.Pos = (300.0,200.0) myBug.radiusres16: Int = 15
Ötökällä on metodi, jolla se "iskee siipiään". Mallinnamme lentämistä nyt yksinkertaisesti muuttamalla ötökän y-koordinaattia parametrin verran pienemmäksi:
myBug.flap(9.5)myBug.posres17: o1.Pos = (300.0,190.5) myBug.flap(20.5)myBug.posres18: o1.Pos = (300.0,170.0)
Ötökän toiminnallisuuteen kuuluu myös alaspäin putoaminen. Metodi fall
kasvattaa
y-koordinaattia kahdella.
myBug.fall()myBug.posres19: o1.Pos = (300.0,172.0) myBug.fall()myBug.posres20: o1.Pos = (300.0,174.0)
Tehtävänanto
Toteuta Bug
-luokka tiedostoon Bug.scala
moduulissa FlappyBug. Sen täytyy
toimia yllä kuvattuun tapaan.
Ohjeita ja vinkkejä
- Huomaa, että sijainnin pitää olla saatavilla juuri nimellä
pos
. Muidenkin luokan käyttäjälle merkityksellisten nimien pitää olla juuri pyydetyt (radius
,fall
,flap
). Sama pätee myös muihin harjoitustehtäviin, joissa pyydetään toteuttamaan luokka tai olio juuri spesifikaation mukaiseksi.- Sijainti vaihtuu ja olkoon siis
var
. Säde ei ja olkoonval
.fall
jaflap
ovat metodeita elidef
.
- Sijainti vaihtuu ja olkoon siis
- Pyydetty luokka muistuttaa monin tavoin edellä laadittua
Obstacle
-luokkaa. - Huomaa, että metodi
fall
on vaikutuksellinen ja parametriton. Sovella äsken opetettua sulutussääntöä. - Voit käyttää toteutuksessasi tiedoston
constants.scala
vakioita maagisten lukujen sijaan. - Muista testatessasi avata REPLiin juuri FlappyBug-moduuli.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
FlappyBug-tehtävä (osa 2/17: Game
-luokka)
Yksi Game
-tyyppinen olio kokoaa yhteen tietyn pelitilanteen, tässä versiossamme siis
yhden ötökän ja yhden esteen. Game
-olio myös tarjoaa toiminnot tuon pelitilanteen
muuttamiseen. Se esimerkiksi määrittää, mitä tapahtuu, kun aikaa kuluu: tällöin pelin
ötökkä putoaa ja este liikkuu. Apuna Game
-olio käyttää viittauksia toisiin olioihin,
joita se komentaa.
Peliolion toivottu toimintatapa
Uuden Game
-olion luominen käy yksinkertaisesti ilman konstruktoriparametreja:
val testGame = new GametestGame: o1.Game = o1.Game@10eb1721
Näin luotu olio kuvaa pelin alkutilannetta, joka on aina samanlainen: pelissä on ötökkä sijainnissa (100,40) ja este, jonka säde on 70, sijainnissa (1000,100).
Pelioliolla on val
-ilmentymämuuttujat nimeltä bug
ja obstacle
, jotka viittaavat
ötökkään ja esteeseen:
testGame.bugres21: o1.Bug = center at (100.0,40.0), radius 15 testGame.obstacleres22: o1.Obstacle = center at (1000.0,100.0), radius 70
Parametrittomalla metodilla timePasses
pelitilanne etenee ötökkää pudottamalla ja
estettä liikuttamalla:
testGame.timePasses()testGame.bugres23: o1.Bug = center at (100.0,42.0), radius 15 testGame.obstacleres24: o1.Obstacle = center at (990.0,100.0), radius 70
Lisäksi Game
-oliolla on parametriton metodi activateBug
, jolla on tarkoitus
reagoida pelaajan komentoihin. Kun pelin activateBug
-metodia kutsutaan, se
ohjeistaa pelin (ainoan) ötökän käyttämään siipiään voimakkuudella 15:
testGame.activateBug()testGame.bugres25: o1.Bug = center at (100.0,27.0), radius 15
Tehtävänanto
Toteuta kuvattu Game
-luokka tiedostoon Game.scala
.
Ohjeita ja vinkkejä
Game
-luokka ei tarvitse konstruktoriparametreja lainkaan. Luokan rungon voi kirjoittaa aaltosulkeisiin heticlass Game
-alustuksen perään. Näin onkin tehty annetussa pohjakoodissa.
Mietittävää
Mitä seuraisi, jos epähuomiossa tulisitkin
määritelleeksi ilmentymämuuttujien sijaan
tuon nimiset metodit? (Vrt. Aarre
-tehtävä
edellä.)
- Sinun täytyy siis määritellä kaksi ilmentymämuuttujaa (nimiksi
bug
jaobstacle
) sekä kaksi metodia (nimiksiactivateBug
jatimePasses
).- Muista, että vaikka luokat nimetäänkin
isolla kuten
Bug
, noilla muuttujilla tulisi olla pienellä alkavat nimet kutenbug
.
- Muista, että vaikka luokat nimetäänkin
isolla kuten
- Laita ilmentymämuuttujat viittaamaan
Bug
- jaObstacle
-olioihin. Luo nuo oliotGame
-luokan koodissa.- Älä siis kopioi koodia
Bug
- taiObstacle
-luokan sisältöäGame
-luokkaan. Vaan käytä noita luokkiaGame
-luokan koodissa: luo niistä kummastakin yksi olionew
-sanalla. - Voit käyttää
new
-käskyä ilmentymuuttujiakin määritellessäsi tuttuun tyyliin:val muuttuja = new Luokka(parametrit)
- Jos tämän kanssa on hankaluuksia,
voi apua olla valinnaisesta
Avaruus
-esimerkistä yllä.
- Älä siis kopioi koodia
- Kun toteutat metodit
activateBug
jatimePasses
, älä suinkaan lähde toteuttamaan ötököille ja esteille aiemmin määriteltyjä toimintoja (esim. laskemaan koordinaateilla). Sen sijaan: kutsuBug
-olion jaObstacle
-olion metodeita! - Käytä taas tyhjiä kaarisulkeita parametrittomien mutta vaikutuksellisten metodien kohdalla.
- Voit käyttää tiedoston
constants.scala
vakioita. Voit myös halutessasi määritellä sinne lisää vakioita ja käyttää niitä. - Ei haittaa, jos et vielä täysin hahmota, miten tätä luokkaa nyt sitten voi hyödyntää graafisen pelin osana. Pelistämme puuttuu vielä käyttöliittymä, mutta laadimme sen pian.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
package
-määrittelyistä
Monen jo muokkaamasi kooditiedoston alussa on rivi, jossa lukee package
-alkuinen
merkintä kuten tämä:
package o1
Merkintä on varsin itseselitteinen: package
-sanan avulla kirjataan kunkin
Scala-kooditiedoston alkuun, mihin pakkaukseen kyseisen tiedoston sisältö kuuluu.
Ohjelman osat, jotka on näin määritelty osaksi samaa pakkausta, voivat käyttää
toisiaan ilman erillistä import
-käskyä tai pakkauksen nimen mainitsemista.
Esimerkiksi äsken et tarvinnut Game
-luokan määrittelyssä import
-käskyä, koska
Game
, Bug
ja Obstacle
ovat kaikki samassa pakkauksessa.
package
-määrittelyt ovat tarpeellisia mutta rutiininomaisia. Niitä ei useimpiin
tämän kurssimateriaalin koodinpätkiin ole kirjoitettu mukaan. (Oheismoduulien
koodissa ne toki ovat.)
Toistaiseksi olemme sysänneet lähes kaiken koodin yleispakkaukseen o1
, mutta
kurssin edetessä jaottelemme koodimme huolellisemmin.
Yhteenvetoa
- Muuttujia voi ryhmitellä erilaisilla tavoilla:
var
vaival
? Mikä tietotyyppi? Paikallinen vai ilmentymämuuttuja? Mikä käyttötapa eli rooli? - Noin kymmenellä roolinimikkeellä voidaan osuvasti kuvata valtaosaa
muuttujista, joita ohjelmissa käytetään.
- Kiintoarvoja käytetään mm. olioiden pysyvien ominaisuuksien kuvaamiseen, kokoojia muodostamaan tuloksia usean arvon perusteella, tilapäissäilöjä välitulosten tallentamiseen hetkeksi ja tuoreimman säilyttäjiä mm. sellaisten ominaisuuksien kuvaamiseen, joiden arvon voi vaihtaa toiseksi.
- Vakioiksi sanotaan kiintoarvoja, joiden arvo tunnetaan jo ennen
ohjelma-ajoa.
- Vakioilla voi helposti parantaa koodin luettavuutta ja muokattavuutta.
- Niiden nimet on tapana kirjoittaa Scalassa isolla alkukirjaimella.
- Voit käyttää itse laatimiasi luokkia myös ilmentymämuuttujien
tyyppeinä.
- Näin voit luoda yhteyksiä luokkien välille.
- Tällainen ilmentymämuuttuja tarkoittaa, että kunkin tietyntyyppisen olion (esim. tilauksen) tietoihin lukeutuu viittaus toiseen olioon (esim. asiakkaaseen).
- Scalassa pitää olla tarkkana kaarisulkeiden kanssa parametrittomissa metodeissa.
- Lukuun liittyviä termejä sanastosivulla: muuttuja; muuttujan rooli, kiintoarvo, tilapäissäilö, kokooja, tuoreimman säilyttäjä; vakio, maaginen luku t. arvo; viittaus; malli.
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 ja Nikolas Drosdek yhteistyössä Juha Sorvan, Otto Seppälän, Arto Hellaksen ja muiden kanssa.
Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.