Luku 2.6: Monenlaista käyttöä muuttujille
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
Kolmannen jaottelun voi 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 voi 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.
Käytämme tässä kurssimateriaalissa muuttujien rooleja selkiyttämään ohjelmien suunnittelua. Monissa kurssin esimerkkimoduuleissa on rooleja 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. tiliolion 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 tunnettu asia.
val Pi = 3.141592653589793
val MinimumAge = 18
val DefaultGreeting = "Hello!"
Vakion ei tarvitse olla luku. Siihen voi tallentaa muunkintyyppistä muuttumatonta tietoa.
Vakioilla 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): kun yhtä kohtaa muuttaa, 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 "temppimuuttuja" 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 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.
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 = Tyontekija("Leena Herppeenluoma", 1957, 7000)testi: o1.luokkia.Tyontekija = o1.luokkia.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ä, kun uusi
nimi muodostetaan, 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 vastaavat sellaisia yhtäläisyyksiä muuttujien käytössä, joita kokeneet ohjelmoijat näkevät toisistaan muuten riippumattomienkin ohjelmien välillä.
Sinun opiskelijana ei ole pakko itse käyttää roolinimiä, mutta rooleista 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 kertovat aloittelijalle, mihin muuttujia voi käyttää, ja vinkkaavat, 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.
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?
Lisää rooleja
Pian luvussa 3.1 kohtaat uuden roolin, askeltajan. Kurssilla puhutaan myöhemmin myös säiliöistä (luku 4.2), sopivimman säilyttäjistä (luku 4.2) ja lipuista (luku 5.6).
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 luontiparametreiksi nimi, asiakasnumero sekä sähköposti- ja katupostiosoitteet:
val testihenkilo = Asiakas("T. Testaaja", 12345, "test@test.fi", "Testitie 1, 00100 Testaamo")testihenkilo: o1.luokkia.Asiakas = o1.luokkia.Asiakas@a7de1d
Asiakasoliolta voi pyytää tekstimuotoisen kuvauksen parametrittomalla kuvaus
-metodilla:
println(testihenkilo.kuvaus)#12345 T. Testaaja <test@test.fi>
Tilausoliolle annetaan luontiparametreiksi tilausnumero ja tilaaja. Jälkimmäinen näistä on viittaus asiakasolioon:
val testitilaus = Tilaus(10001, testihenkilo)testitilaus: o1.luokkia.Tilaus = o1.luokkia.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
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.luokkia.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 sisältää viittauksen asiakasolioon, ja asiakasolion
osoite
-ilmentymämuuttuja sisältää 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 + ">"
Kullakin asiakasoliolla on muutama kiintoarvoinen ilmentymämuuttuja, jossa se pitää kirjaa muuttumattomista tiedoistaan.
Hahmotellaan tilauksia kuvaava luokka ensin pseudokoodina:
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 end Tilaus
Tilausolion tulee kerätä yhteen kaikkien tuotelisäysten kokonaisvaikutus hintaan. Siihen sopii kokoojana käytetty ilmentymämuuttuja.
Useasta luokasta koostuvan mallimme kannalta keskeistä on, että tilausolion tiedoista on viitattava asiakasolioon.
Viittauksen avulla voimme sitten esimerkiksi konsultoida asiakasoliota muodostaessamme tilauksen kuvausta.
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"
end Tilaus
Tässä Asiakas
-luokka on Tilaus
-luokan luontiparametrin
tyyppinä, joten tilausoliota luotaessa on jälkimmäiseksi
parametriksi annettava viittaus asiakasolioon. val
-sanalla
ilmoitetaan, että kyseinen viittaus halutaan talteen
ilmentymämuuttujaan.
Lausekkeen 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
end Hirvio
Vaikka emme tätä esimerkkiä oikeaksi peliksi työstäkään, jatketaan 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 = 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
Kullakin aarteella on sen arvoa kuvaava desimaaliluku. Aarteella on myös haastetaso, luku sekin.
Aarretta vartioi sitä voimakkaampi peikko, mitä isompi haastetaso aarreoliolle annetaan.
Aarteelta voi tiedustella sen arvoa ja vartijaa.
Aarretta kuvaa myös sen houkuttelevuusluku, joka saadaan jakamalla aarteen arvo sen vartijan kyseisenhetkisillä kuntopisteillä. Houkuttelevuus siis kasvaa aarteen vartijan heikentyessä. Tässä esimerkissä aarretta vartioi täysivoimainen 50-pistepeikko, joten houkuttelevuus on 1000.0/50 eli 20.0.
Alla on toteutus Aarre
-luokalle.
class Aarre(val arvo: Double, val haaste: Int):
def vartija = Hirvio("peikko", this.haaste)
def houkuttelevuus = this.arvo / this.vartija.nykykunto
override def toString = "aarre (arvo " + this.arvo + "), jota vartioi " + this.vartija
end Aarre
Houkuttelevuus määritellään jakolaskulla vartijan nykykunnon perusteella.
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
end Taivaankappale
Taivaankappaleilla on nimet, säteet ja sijainnit.
Sijainti on 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 = Taivaankappale("Maa", 15.9, Pos(10, this.korkeus / 2))
val kuu = Taivaankappale("Kuu", 4.3, Pos(971, this.korkeus / 2))
override def toString = s"${this.leveys}x${this.korkeus} alue avaruudessa"
end Avaruus
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.
Avaruutta mallintava olio koostuu leveyden ja korkeuden lisäksi
kahdesta 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 = Avaruus(500)avaruus: Avaruus = 1000x500 alue avaruudessa avaruus.maares6: Taivaankappale = Maa avaruus.kuures7: Taivaankappale = Kuu
Loimme Avaruus
-olion, jolloin...
... samalla tuli luoduksi kaksi 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
Avaruutta kuvaavaan olioon on kytketty kaksi
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 mallia 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 esimerkissä alla. 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 = Obstacle(150, Pos(800, 200))bigObstacle: o1.flappy.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.flappy.Obstacle = center at (790.0,200.0), radius 150 bigObstacle.approach()bigObstacle.approach()bigObstacleres14: o1.flappy.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 end Obstacle
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
end Obstacle
Ennen kuin etenemme luokkien Bug
ja Game
pariin, kiinnitä huomio kahteen asiaan:
Metodin toteutukseen on kirjattu maaginen luku 10, joka säätelee esteiden etenemisvauhtia.
Parametrittoman 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:
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
end Obstacle
Käytämme vakion nimeä maagisen luvun sijaan.
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.
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.
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ä, kirjoita 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 = Bug(Pos(300, 200))myBug: o1.flappy.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än sijainti on aluksi juuri se, mikä luontiparametriksi annettiin.
Ainakin tässä versiossa ohjelmastamme millä tahansa ötökällä on aina sama muuttumaton säde, 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
.
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.Jotta ötökkä toimii kuten REPL-esimerkissä yllä, on luokkaan laadittava
toString
-metodi.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:
val testGame = Game()testGame: o1.flappy.Game = o1.flappy.Game@10eb1721
Kun luomme olion luokasta, jolle ei anneta luontiparametreja, kirjoitamme perään vain tyhjät sulkeet.
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.flappy.Bug = center at (100.0,40.0), radius 15 testGame.obstacleres22: o1.flappy.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.flappy.Bug = center at (100.0,42.0), radius 15 testGame.obstacleres24: o1.flappy.Obstacle = center at (990.0,100.0), radius 70
Pelin ötökän ja esteen koordinaatit muuttuivat.
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.flappy.Bug = center at (100.0,27.0), radius 15
Y-sijainti on pienennyt 15 yksikön verran.
Tehtävänanto
Toteuta kuvattu Game
-luokka tiedostoon Game.scala
.
Ohjeita ja vinkkejä
Game
-luokka ei tarvitse luontiparametreja lainkaan. Luokan rungon avaavan kaksoispiteen voi kirjoittaaclass Game
-alustuksen perään. Näin onkin tehty annetussa pohjakoodissa.
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
.
Aseta 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 olio.Voit käyttää olionluontikäskyä ilmentymuuttujia määritellessäsi:
val muuttuja = Luokka(parametrit)
Jos tämän kanssa on hankaluuksia, voi apua olla valinnaisesta
Avaruus
-esimerkistä yllä.
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 parametrittomissa mutta vaikutuksellisissa metodeissa.
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.
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, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, Anna Valldeoriola Cardó 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, Juha Sorva ja Jaakko Nakaza. 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; sitä 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 suunnitteluun ja toteutukseen on osallistunut useita opiskelijoita yhteistyössä O1-kurssin opettajien kanssa.
Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.
Lisäkiitokset tähän lukuun
FlappyBug on saanut vaikutteita Dong Nguyenin pelistä.
Vakioiden nimet on Scalassa tapana kirjoittaa isolla alkukirjaimella (kuten kurssin tyyliopaskin kertoo).