Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 7.2: Piirreluokkia
Tästä sivusta:
Pääkysymyksiä: Miten kuvaan ohjelmassa ylä- ja alakäsitteitä? Miten teen luokista muokattavampia ja yleiskäyttöisempiä?
Mitä käsitellään? Ylä- ja alakäsitteiden kuvaaminen piirreluokkien avulla. Abstraktit metodit ja muuttujat. Staattinen vs. dynaaminen tietotyyppi. Luokan sulkeminen.
Mitä tehdään? Luetaan, ja on siellä ohjelmointitehtäväkin.
Suuntaa antava työläysarvio:? Reilu tunti ilman vapaaehtoisia tehtäviä.
Pistearvo: B35.
Oheisprojektit: Subtypes (uusi).
Johdanto: tasokuvioita
Oletetaan, että olemme laatimassa ohjelmaa, jonka on tarpeen käsitellä tasokuvioita kuten ympyröitä ja suorakaiteita. Ohjelman täytyy muun muassa pystyä laskemaan ympyröiden ja suorakaiteiden pinta-aloja.
Kyse ei siis ole Pic
-tyyppisistä ympyröiden ja suorakaiteiden kuvista, vaan nyt
mallinnamme geometrisia käsitteitä.
Kuvataan ympyröitä tällaisella luokalla Circle
:
import scala.math.Pi
class Circle(val radius: Double) {
def area = Pi * this.radius * this.radius
// jne. muita ympyröiden metodeita
}
Kuvataan suorakaiteita puolestaan luokalla Rectangle
:
class Rectangle(val sideLength: Double, val anotherSideLength: Double) {
def area = this.sideLength * this.anotherSideLength
// jne. muita suorakaiteiden metodeita
}
Esitetyissä Circle
- ja Rectangle
-luokissa ei yksittäisinä luokkina tarkasteltuina ole
vikaa. Ne voisivat kyllä toimia toisistaan riippumattomasti jonkin ohjelman osina...
Mutta #1
Mitä jos haluamme lisäksi vertailla kuvioita pinta-alan perusteella toisiin kuvioihin? Yksi tapa olisi kirjoittaa luokkiin metodeita näin:
import scala.math.Pi
class Circle(val radius: Double) {
def area = Pi * this.radius * this.radius
def isBiggerThan(another: Circle): Boolean = this.area > another.area
def isBiggerThan(rectangle: Rectangle): Boolean = this.area > rectangle.area
}
class Rectangle(val sideLength: Double, val anotherSideLength: Double) {
def area = this.sideLength * this.anotherSideLength
def isBiggerThan(another: Rectangle): Boolean = this.area > another.area
def isBiggerThan(circle: Circle): Boolean = this.area > circle.area
}
Pitääkö tosiaan kumpaankin luokkaan kirjoittaa tällaiset kaksi aivan samankaltaista vertailumetodia? Ei kovin DRY ratkaisu. Ja mitä jos kuviotyyppejä on enemmän kuin kaksi?
Eikä siinä vielä kaikki.
Mutta #2
Mitä jos haluamme tallentaa samaan kokoelmaan viittauksia erimuotoisiin kuvioihin: ympyröihin, suorakaiteisiin ja ehkä muihinkin kuvioihin? Vaikkapa näin:
object ShapeTest extends App { val shapes = Buffer[?????]() shapes += new Circle(10) shapes += new Rectangle(10, 100) shapes += new Circle(5) var sumOfAreas = 0.0 for (current <- shapes) { sumOfAreas += current.area } println("Pinta-alojen summa on: " + sumOfAreas) }
current
-muuttujan tyyppi?
"Joku sellainen, jolle voi kutsua area
-metodia"?Ei kai sentään ole pakko tehdä erillisiä listoja ympyröistä, suorakaiteista jne.? Se ei olisi kovin käytännöllistä.
Saamme lisävaloa ongelmaan, kun alustamme puskurin sisällön samalla kun luomme puskurin. Siis näin:
object ShapeTest extends App {
val shapes = Buffer(new Circle(10), new Rectangle(10, 100), new Circle(5))
var sumOfAreas = 0.0
for (current <- shapes) {
sumOfAreas += current.area
}
println("Pinta-alojen summa on: " + sumOfAreas)
}
Buffer[AnyRef]
. Siihen, mikä AnyRef
on, palaamme seuraavassa
luvussa 7.3.)current
viitata millaiseen vain olioon. Kutsu current.area
tuottaa nyt käännösaikaisen virheilmoituksen, joka kertoo
suunnilleen: "Ei tuollaista metodia ole millä tahansa oliolla."On totta, että millä tahansa olioilla ei ole area
-nimistä metodia, ja siksi on mainiota,
että kääntäjä osaa kyseenalaistaa metodikutsun current.area
. Sellaisen puskurin alkiolle,
joka sisältää "mitä vaan olioita" ei tosiaan pidä mennä noin vain kutsumaan area
-metodia.
Toisaalta tässä tapauksessa tiedämme, että olemme laittaneet puskuriin nimenomaan
Rectangle
- ja Circle
-olioita, joita yhdistää (ainakin) se, että niillä tällainen
metodi on. Olisi luonnollista ja kätevää, jos yllä oleva ShapeTest
-koodi toimisi.
Mikä neuvoksi?
Ylä- ja alakäsitteistä
Ihminen mieltää, että ympyrät ja suorakaiteet ovat kuvioita ja että pinta-ala on tällaisten tasokuvioiden yleinen ominaisuus. Meille on luonnollista, että tietty asia voi olla ilmentymä sekä spesifisemmästä käsitteestä (kuten ympyrä) että sen yläkäsitteestä (kuten kuvio). Voimme myös ajatella, että:
- "Tämä ohjelma laskee kuvioiden kokonaispinta-alan." tai
- "Metodi
area
on kaikilla kuvioilla." tai - "Metodi
isBiggerThan
ottaa parametrikseen minkä tahansa toisen kuvion."
Voimme mieltää ja piirtää luokkien väliset suhteet tähän tapaan:
Ala- ja yläkäsitteen välillä on niin sanottu is a -suhde: "Every circle is a shape."
On myös olemassa tapoja esittää tällaisia ajatuksia tietokoneohjelmassa. Yhtä sellaista — piirreluokkia — käsitellään tässä luvussa. Toista — yliluokkia — käsitellään luvussa 7.3.
Piirreluokka yläkäsitteenä
Määritellään kuvion käsitettä vastaava tietotyyppi Shape
.
trait Shape {
def isBiggerThan(another: Shape) = this.area > another.area
def area: Double
}
Shape
. Tämän piirreluokan ajatuksena on
määritellä millaisia kuviot yleisesti ottaen ovat: "kaikilla
olioilla, joita voi tässä ohjelmassa sanoa kuvioiksi — olivat
ne sitten muuten minkälaisia tahansa — on seuraavanlaiset metodit".trait
sanan
class
sijaan, mutta muuten se muistuttaa kovasti tutunlaisia
luokkamäärittelyjä.isBiggerThan
-metodi,
jolla voi verrata kuvioiden pinta-aloja keskenään.Shape
, eli tälle metodille annetaan
parametriksi juuri sellainen kuvio-olio, jollaisia tämä
piirreluokka kuvaa. Huomaat: piirreluokkaa voi käyttää
tietotyyppinä siinä missä tavallisia luokkiakin.area
-metodi
pinta-alan laskemiseen. Mutta: tässä onkin määritelty vain
metodin nimi, parametrit (joita ei tässä tapauksessa ole) ja
palautusarvon tyyppi (Double
kaksoispisteen perässä).Toisin kuin nähdynlaisiin tavallisiin luokkiin, piirreluokkiin voi määritellä
abstrakteja, toteutuksettomia metodeita. Esimerkiksi Shape
-piirreluokassa
ilmoitetaan, että minkä tahansa kuvion pinta-ala on mahdollista laskea area
-nimisellä
parametrittomalla metodilla ja tuloksena saadaan Double
-arvo. Kuitenkin metodin
toteutus on jätetty auki; se on tarkoitus määritellä kullekin kuviotyypille erikseen.
Alakäsitteen määrittely
Yläkäsitteen Shape
-määrittely on nyt kunnossa, mutta on määrittelemättä, että ympyrät
ja suorakaiteet ovat kuvioita. Tehdään se näin:
import scala.math.Pi
class Circle(val radius: Double) extends Shape {
def area = Pi * this.radius * this.radius
}
class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape {
def area = this.sideLength * this.anotherSideLength
}
extends
.Circle
laajentaa piirreluokan Shape
kuvaamaa tietotyyppiä." Tai:
"Luokkaan Circle
on liitetty (mixed in) piirre Shape
."
Tai: "Kaikki Circle
-tyyppiset oliot ovat paitsi Circle
jä
myös Shape
-tyyppisiä, ja niillä on myös Shape
-piirreluokan
kuvaamat ominaisuudet."Huom. Ympyröillä ja suorakaiteilla on extends Shape
-määrittelyn johdosta myös
isBiggerThan
-metodi, vaikka sitä ei ole näiden luokkien määrittelyyn kirjoitettukaan.
Piirreluokan tyyppiset oliot
Edellisen perusteella voimme todeta:
- Piirreluokat kuvaavat tietotyyppejä kuten tavalliset luokatkin.
- Piirreluokan nimeä voi käyttää esimerkiksi muuttujan tyyppinä kuten luokankin nimeä.
- On olemassa
Shape
-tyyppisiä olioita.
Voiko siis Shape
-olion luoda käskyllä new Shape
? Ja mitä silloin syntyy? Kokeillaan:
new Shape<console>:12: error: trait Shape is abstract; cannot be instantiated
Ei ole olemassa Shape
-olioita, jotka olisivat "vaan kuvioita", eikä sellaista voi luoda
new
-operaattorilla. Ja hyvä niin, koska ei missään ole määritelty moisille olioille
area
-metodin toteutustakaan.
Piirreluokasta luodaan ilmentymiä epäsuorasti sen alakäsitteitä kuvaavien luokkien kautta, kuten seuraava REPL-kokeilu kertoo. Aloitetaan luomalla ympyrä:
val ympyra = new Circle(1)ympyra: o1.shapes.Circle = o1.shapes.Circle@1a1a02e
Käytetään nyt entuudestaan tuntematonta metodia isInstanceOf
, joka on käytettävissä
kaikilla Scala-olioilla. Tämän metodin avulla voi selvittää, onko olio tiettyä tyyppiä.
ympyra.isInstanceOf[Circle]res0: Boolean = true
isInstanceOf
-metodille annetaan
tyyppiparametri hakasuluissa.ympyra
-muuttujan osoittamalta oliolta,
onko se Circle
-tyyppinen. Onhan se.Tutkitaan nyt, onko kyseinen olio Shape
-tyyppinen:
ympyra.isInstanceOf[Shape]res1: Boolean = true
Saatiin taas true
. Näemme, että oliolla voi Scalassakin olla yhtä aikaa monta tyyppiä
niin kuin halusimmekin.
Oliomme ei suinkaan ole kaikkia tyyppejä. Se ei esimerkiksi ole Rectangle
:
ympyra.isInstanceOf[Rectangle]<console>:13: warning: fruitless type test: a value of type o1.shapes.Circle cannot also be a o1.shapes.Rectangle ympyra.isInstanceOf[Rectangle] ^ res2: Boolean = false
Piirreluokka alkioiden tyyppinä
Vector(new Circle(1), new Circle(2))res3: Vector[o1.shapes.Circle] = Vector(o1.shapes.Circle@e17571, o1.shapes.Circle@1e56bea) Vector(new Circle(1), new Rectangle(2, 3))res4: Vector[o1.shapes.Shape] = Vector(o1.shapes.Circle@876228, o1.shapes.Rectangle@3d619a)
Circle
.Shape
.Ratkaisuja johdanto-ongelmiin
Piirreluokan avulla johdannossa esitetyt ongelmat katoavat:
isBiggerThan
-metodi kaikille kuvioille
Edellä luonnostelimme useita isBiggerThan
-metodeita Circle
- ja Rectangle
-luokkiin.
Nyt kuitenkin käytössämme on yläkäsite Shape
. Siihen määritelty isBiggerThan
-metodi
(toistettuna alla) kelpaa sekä ympyröiden että suorakaiteiden — ja kaikkien mahdollisesti
tulevien lisäkuviotyyppien! — pinta-alavertailuun. Vertailu onnistuu joustavasti niin
yhdenmuotoisten kuvioiden kesken kuin ristiin erimuotoisten välilläkin.
trait Shape {
def isBiggerThan(another: Shape) = this.area > another.area
def area: Double
}
Kaksi area
-metodikutsua isBiggerThan
-metodissa ovat mahdollisia siksi, että pari riviä
alempana oleva määrittely takaa, että olivatpa isBiggerThan
-metodikutsun vastaanottanut
this
-olio ja parametriksi annettu another
-olio millaisia vain kuvioita, niin niiltä
väistämättä löytyy jollain tavalla toteutettu area
-metodi.
Toteutuspakko!
Jotta luokista Circle
ja Rectangle
voi luoda ilmentymiä, on näille ilmentymille pakko
toteuttaa niihin liitetyn piirreluokan Shape
abstrakti metodi area
.
Jos yllä kuvatuista Circle
- ja Rectangle
-luokista jättäisi area
-metodin toteutuksen
pois, syntyisi käännösaikaisia virheilmoituksia, joissa valitetaan, ettei kyseistä metodia
ole toteutettu. Scala-työkalut siis pitävät huolen siitä, että konkreettisilta olioilta
löytyy toteutukset piirreluokkien abstrakteille metodeille. Voimme luottaa siihen, että
tietyntyyppiselta oliolta löytyvät tietyt metodit.
ShapeTest
kin toimii
Myös ShapeTest
-esimerkkiohjelma toimii nyt alkuperäisessä muodossaan:
object ShapeTest extends App {
val shapes = Buffer(new Circle(10), new Rectangle(10, 100), new Circle(5))
var sumOfAreas = 0.0
for (current <- shapes) {
sumOfAreas += current.area
}
println("Pinta-alojen summa on: " + sumOfAreas)
}
Shape
. Tyypinhän saisi
myös kirjata erikseen itsekin muodossa Buffer[Shape](
...)
,
jos siltä tuntuisi.area
-kutsu on nyt mahdollinen, koska current
on
tyyppiä Shape
ja piirreluokka takaa kaikilla Shape
-olioilla
olevan toteutus area
-metodille.Pohdintatehtävä tähän väliin
Staattinen vs. dynaaminen tyyppi
Kerrataan luvusta 1.2 pari termiä:
- Sanalla staattinen viitataan ohjelmoinnissa usein siihen "olomuotoon", joka ohjelmalla on (myös) silloin, kun sitä ei ajeta. Usein tällä sanalla viitataan siis sellaisiin seikkoihin, jotka näkyvät suoraan ohjelmatekstistä tai ovat pääteltävissä sen perusteella.
- Sanalla dynaaminen viitataan ohjelmoinnissa usein siihen "olomuotoon", joka ohjelmalla on, kun sitä ajetaan: ohjelmakoodin ajamisprosessiin ja ohjelma-ajon aikana tapahtuviin asioihin. Ohjelman dynaamisia ominaisuuksia ei voi kaikilta osin päätellä ohjelmakoodista, vaan niihin voivat vaikuttaa esimerkiksi ohjelman käyttäjän antamat syötteet.
Luvussa 1.3 todettiin, että lausekkeilla on arvot ja näillä arvoilla tyypit. Luvussa 1.4 taas, että muuttujilla on tyyppi. Luku 1.4 myös kertoi, että kun muuttujaan sijoitetaan jonkin lausekkeen arvo, on tuon arvon tyypin oltava muuttujan tyypin kanssa yhteensopiva. Tähän asti kurssilla on pitkälti voinut ajatella, että "yhteensopivuus" tarkoittaa "lausekkeen arvolla on oltava sama tyyppi kuin muuttujalla". Nyt piirreluokkien myötä on syytä tarkentaa asiaa hieman.
Staattisesti tyypitetyissä kielissä kuten Scala voidaan tehdä ero staattisen ja dynaamisen tietotyypin välillä. Niistä seuraavaksi.
Staattinen tyyppi
Staattinen tyyppi (static type) voidaan selvittää ohjelmakoodia tutkimalla, ohjelmaa ajamatta, eikä siihen vaikuta esimerkiksi ohjelmalle annettu syöte. Tässä esimerkkejä:
- Metodien parametreille kirjataan kaksoispisteen perään parametrimuuttujien staattiset tyypit.
- Sijoituksesta
val teksti = "laama"
voidaan suoraan päätellä muuttujan staattisen tyypin olevanString
. - Summalausekkeen
1 + 1
voidaan päätellä olevan staattiselta tyypiltäänInt
sillä perusteella, että myös molempien osalausekkeina olevien literaalien staattinen tyyppi onInt
.
Staattiset tyypit on muuttujilla ja lausekkeilla. Siis: ei lausekkeiden arvoilla vaan koodissa olevilla ilmaisuilla itsellään!
Staattinen tyyppi määrää, mitkä operaatiot ja metodikutsut sallitaan ja mitä ei.
Esimerkiksi metodikutsusta jokuOlio.jokuMetodi()
seuraa käännösaikainen virheilmoitus,
mikäli muuttujan jokuOlio
staattinen tyyppi ei ole sellainen, jolle on määritelty
parametriton metodi jokuMetodi
.
Dynaaminen tyyppi
Muuttujiin tallennetuilla arvoilla ja lausekkeiden arvoilla on dynaamiset tyypit (dynamic type). Näihin tyyppeihin vaikuttaa se, mitä ohjelma-ajon aikana tapahtuu.
Usein arvojen dynaamiset tyypit ovat samat kuin vastaavien muuttujien ja lausekkeiden
tyypit. Esimerkiksi kokonaislukujen summalauseke 1 + 1
evaluoituu kakkosta kuvaavaksi
arvoksi, jonka dynaaminen tyyppi on Int
kuten lausekkeen staattinenkin tyyppi.
Ero staattisen ja dynaamisen tyypin väliltä löytyy esimerkiksi seuraavasta ohjelmanpätkästä:
var test: Shape = new Rectangle(10, 20)
println(test.area)
test = new Circle(10)
println(test.area)
val selected = readLine("Haluatko ympyrän? Sano 'joo', jos haluat, muuten tulee neliö. ")
if (selected == "joo") {
test = new Circle(readLine("Säde: ").toInt)
} else {
val sivu = readLine("Sivu: ").toInt
test = new Rectangle(sivu, sivu)
}
println(test.area)
test
määritellään tässä Shape
-tyyppiseksi, jolloin
siihen voi tallentaa viittauksen mihin tahansa olioon, jonka
tyyppiin on liitetty Shape
-piirre.test
staattinen tyyppi on siis Shape
.
Kaikki esimerkin metodikutsut test.area
ovat sallittuja,
koska Shape
-tyypille on määritelty area
-metodi.test
-lausekkeen arvon dynaaminen tyyppi eroaa
staattisesta tyypistä. Tätä esimerkkikoodia ajettaessa muuttuja
viittaa Rectangle
-tyyppiseen olioon, sitten Circle
en.
Viimeisellä rivillä test
-lausekkeen arvon dynaaminen tyyppi
riippuu käyttäjän aiemmin antamasta syötteestä.Kullakin area
-metodin kutsukerralla se, mikä metoditoteutus suoritetaan, riippuu arvon
dynaamisesta tyypistä. Sanotaan: metodien nimet on dynaamisesti sidottu (dynamically
bound) metoditoteutuksiin. Tai epävirallisemmin: tapa, jolla lähetettyyn viestiin
reagoidaan, riippuu vastaanottajaolion tyypistä.
Arvon dynaamisen tyypin ei siis tarvitse olla identtinen vaan tyyppiyhteensopiva
(type compatible) staattisen tyypin kanssa. Viittaus Circle
-olioon voi olla Circle
-
tai Shape
-tyyppisessä muuttujassa muttei String
- tai Obstacle
-tyyppisessä.
Tyypit REPLissä
Tässä vielä muutama aiheeseen liittyvä käsky REPLissä. Kiinnitä huomiosi siihen, miten REPLin tulosteissa näkyvät toisaalta muuttujien staattiset tyypit yhtäsuuruusmerkin vasemmalla puolella ja toisaalta arvojen dynaamiset tyypit oikealla osana olioiden kuvauksia.
var test1 = new Rectangle(5, 10)test1: o1.shapes.Rectangle = o1.shapes.Rectangle@38c8ed
test1
-muuttujan (staattinen) tyyppi tulee päätellyksi siihen sijoitettavan alkuarvon dynaamisen
tyypin perusteella. Se on siis Rectangle
, eikä muuttujaan voi sijoittaa viittausta ympyräolioon.
Virheilmoitus on suhteellisen selväsanainen:
test1 = new Circle(10)<console>:11: error: type mismatch; found : o1.shapes.Circle required: o1.shapes.Rectangle test1 = new Circle(10) ^
Kuitenkin jos muuttujalle erikseen määrätään tyypiksi Shape
, niin staattinen tyyppi on "laajempi"
kuin arvon dynaaminen tyyppi:
var test2: Shape = new Rectangle(5, 10)test2: o1.shapes.Shape = o1.shapes.Rectangle@bdee1c
Tällaiseen muuttujaan voi sijoittaa myös ympyräviittauksen:
test2 = new Circle(10)test2: o1.shapes.Shape = o1.shapes.Circle@1071884
Pikkutehtävä: piirreluokat ja tietotyypit
Mitä hyötyä tuosta rajoituksesta on?
Tässä kappaleessa kerrotaan tarkemmin, miksi käytettävissä olevien toimintojen
rajoittaminen nimenomaan staattisen tyypin perusteella on järkevää. Siis: miksi on
määritelty niin, että esimerkiksi äskeisen tehtävän lopussa radius
-arvoa ei voinut
käyttää? Jos asia jo tuntuu aivan perustellulta, voit ohittaa kappaleen ja siirtyä
eteenpäin.
Asialla on käytännön merkitystä muun muassa kaikissa sellaisissa metodeissa, joilla
on Shape
tai muu yläkäsite parametrin tyyppinä eli parametrimuuttujan staattisena
tyyppinä. Tälläinen metodihan on esimerkiksi Shape
-piirreluokan isBiggerThan
:
trait Shape {
def isBiggerThan(another: Shape) = this.area > another.area
def area: Double
}
another
-muuttuja osoittaa erääseen kuvio-olioon, ja muuttujan
staattinen tyyppi on Shape
. Kutsu another.area
on sallittu
juuri siksi, että area
on staattisen tyypin mukainen metodi.another.radius
, koska sädettä ei
ole kaikilla mahdollisilla olioilla, jotka voi välittää
parametriksi isBiggerThan
-metodille. Jos moinen kutsu
sallittaisiin, ei metodin suorittaminen onnistuisikaan, mikäli
parametriksi annettaisiin vaikkapa suorakaide. Ongelma ilmenisi
vasta ohjelma-ajon aikana.another
in lisäksi this
-muuttujalle, jonka
staattinen tyyppi on Shape
siksi, että koodi on kirjoitetettu
Shape
-piirreluokkaan.Yleisemmin sanoen: kun parametrin tyyppinä on yläkäsite, voi metodi kohdistaa siihen vain yläkäsitteen määräämiä toimintoja. Näin taataan, että metodi toimii kaikille eri alakäsitteiden ilmentymille.
Staattisen tyypin avulla työkalut (eritoten kääntäjä) voivat paikantaa virheitä paremmin ja jo ennen ohjelma-ajoa. Ellei staattinen tyyppi rajaisi toimintoja, ei tietotyyppien kirjaamisesta koodiin olisi läheskään niin paljon iloa. (Vrt. teksti staattisesta ja dynaamisesta tyypityksestä luvun 1.8 lopussa.)
Mainitun rajoituksen kiertäminen
Mitä jos nyt kuitenkin on vaikkapa viittaus Circle
-olioon tallennettuna
Shape
-tyyppiseen muuttujaan ja halutaan kutsua sen radius
-metodia? Tai mitä jos
halutaan tehdä valinta ohjelman ajon aikana sen perusteella, viittaako Shape
-tyyppinen
muuttuja tietyllä hetkellä ympyrään vai johonkin muuhun kuvioon?
match
-käsky (luku 4.2) tulee avuksi. Se tekee valinnan nimenomaan dynaamisen
tyypin perusteella. Näin:
var someShape: Shape = new Circle(10)someShape: Shape = Circle@1de2de2 someShape match { case someCircle: Circle => println("It's a circle and its radius is " + someCircle.radius) case _ => println("It's not a circle.") }It's a circle and its radius is 10.0
someShape
ssa on Circle
-tyyppinen
arvo, tuo arvo tulee tallennetuksi someCircle
-nimiseen
muuttujaan. someCircle
n staattinen tyyppi on Circle
.case _
voi lukea "missä tahansa muussa
tapauksessa" (luku 4.3).Tiedoksi kiinnostuneille: huonompi tapa
Toinen mutta yleensä huonompi mahdollisuus on käyttää asInstanceOf
-metodia,
joka on kaikilla Scala-olioilla. Huomaa tyyppiparametri ja palautusarvon
staattinen tyyppi:
someShape.asInstanceOf[Circle]res5: Circle = Circle@1de2de2 someShape.radius<console>:14: error: value radius is not a member of Shape someShape.radius ^ someShape.asInstanceOf[Circle].radiusres6: Double = 10.0
Varoitus! Tämä aiheuttaa ajonaikaisen virhetilanteen, mikäli kuviomuuttuja
ei viittaa ympyrään. Kääntäjä ei pysty tarkastamaan asiaa luotettavasti.
Käyttämällä asInstanceOf
-metodia ohjelmoija ohittaa kielen vahvan
tyypityksen ja heikentää ohjelman tyyppiturvallisuutta. Siirtyy täysin
ohjelmoijan harteille varmistaa, että kyseisen olion dynaaminen tyyppi
todella vastaa sitä, mitä koodiin on asInstanceOf
-sanan perään
hakasulkeisiin kirjoitettu. Tätä menetelmää kannattaa käyttää vain
harkitusti ja harvoin.
asInstanceOf
-metodi vastaa monesta muusta kielestä löytyviä
tyyppimuunnoksia (type cast tai vain cast).
Ohjelmointitehtävä: käytä ja muokkaa piirrettä
Osa 1/2: uusi kuviotyyppi
Tee kuvioille uusi alakäsite, suorakulmainen kolmio, jota kuvaa luokka RightTriangle
.
Liitä luokkaan Shape
-piirre. Suorakulmaisilla kolmioilla pitäisi siis olla kaikki
kuvioiden yleiset ominaisuudet, minkä lisäksi niille tulee hypotenuse
-metodi. Haluttua
toiminnallisuutta valottaa tämä REPL-esimerkki:
val triangle = new RightTriangle(3.0, 4.0)triangle: o1.shapes.RightTriangle = o1.shapes.RightTriangle@18bcb2d triangle.hypotenuseres7: Double = 5.0 triangle.areares8: Double = 6.0 new Circle(3).isBiggerThan(triangle)res9: Boolean = true triangle.isBiggerThan(new Rectangle(7, 5))res10: Boolean = false
Huom. isBiggerThan
-metodia ei tässä tehtävässä saa eikä kannata kirjoittaa
RightTriangle
-luokkaan. Kolmiotkin saavat sen käyttöönsä Shape
-piirreluokasta.
Osa 2/2: piirinlaskemismetodi kaikille kuvioille
- Lisää
Shape
-piirreluokkaan uusi abstrakti, parametriton metodiperimeter
, joka laskee ja palauttaa kuvion piirin (eli reunaviivan kokonaispituuden)Double
-tyyppisenä lukuna. - Lisättyäsi abstraktin metodin ja tallennettuasi
Shape.scala
n huomaat Eclipsen paheksuvan: alakäsitteiden määrittelyt eivät enää kelpaa, koska niistä puuttuu toteutusperimeter
-metodille. - Kirjoita
perimeter
-metodille toteutus paitsi uuteen luokkaasiRightTriangle
myös luokkiinCircle
jaRectangle
. (Projektissa on mukana myös luokka nimeltäSquare
, johon palaamme myöhemmin.) Käyttöesimerkkejä:
triangle.perimeterres11: Double = 12.0 new Circle(5).perimeterres12: Double = 31.41592653589793 new Rectangle(2, 5).perimeterres13: Double = 14.0
Palauttaminen
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Vapaaehtoisia lisäyksiä
Voit lisätä ShapeTest
-ohjelman kuvioita sisältävään puskuriin
kolmion tai useita. Sen pitäisi onnistua helposti, jos olet laatinut
RightTriangle
-luokan oikein.
Voit myös tehdä RightTriangle
-luokkaan ja muihin kuvioluokkiin
toString
-metodit, jotta kuvioita on kätevämpi käsitellä REPLissä.
Tai Shape
-luokkaan yleisluontoisen toString
-metodin kaikille
kuvioille.
Pohdintaa piirreluokkien eduista
Piirreluokat ovat käteviä, ja niillä voi usein parantaa ohjelman laatua merkittävästikin. Esimerkiksi:
- Ohjelman osien yleiskäyttöisyys paranee. Samalle metodille
voi antaa dynaamiselta tyypiltään toisistaan poikkeavia
parametreja, ja metodi toimii niille kaikille. Yksi esimerkki
on
isBiggerThan
yllä. - Muokattavuus paranee. Koodia, joka on vähemmän toisteista,
on helppo ylläpitää. Lisäksi uusien alakäsitteiden lisääminen
on helppoa. Kun esimerkiksi lisäsit uudeksi kuviotyypiksi
kolmiot, niin riitti kirjoittaa maininta
extends Shape
sekäarea
-metodin toteutus. Tällöin uudentyyppisille olioillekin voi kutsuaisBiggerThan
-metodia, ja niitä voi käyttää missä vain yhteydessä, jossaShape
-olioita muutenkin käytetään.
Kokoava pikkutehtävä
Useita yläkäsitteitä
Yläkäsitteitä eri tasoilla
On mahdollista liittää piirreluokka toiseen piirreluokkaan. Tällä tavoin voit määritellä piirreluokkienkin välille ylä- ja alakäsitesuhteita.
Esimerkiksi näin:
trait PersonAtAalto {
// Tänne metodeita ja/tai ilmentymämuuttujia, jotka ovat yhteisiä
// Aallossa työskenteleville/opiskeleville/vieraileville henkilöille.
}
trait Employee extends PersonAtAalto {
// Tänne metodeita ja/tai muuttujia työntekijäolioille. Kaikilla
// työntekijäolioilla on ne PersonAtAalto-piirreluokassa määriteltyjen lisäksi.
}
Nyt kun määritellään luokka TeachingAssistant
siten, että sillä on piirre Employee
, niin
assarit ovat sekä työntekijöitä että henkilöitä:
class TeachingAssistant extends Employee {
// Assariolioilla on kaikki ne ilmentymämuuttujat ja metodit,
// jotka on määritelty Employee-piirreluokassa ja lisäksi kaikki
// ne, jotka on määritelty PersonAtAalto-piirreluokassa.
// Lisäksi täällä voi määritellä muita muuttujia ja metodeita
// nimenomaan assariolioille.
}
Useita välittömiä yläkäsitteitä
Äskeisessä esimerkissä luokalla oli välittömänä yläkäsitteenä piirreluokka ja epäsuorasti myös tuon piirreluokan yläkäsite. On myös mahdollista kirjata yhden luokan määrittelyyn useita piirreluokkia välittömiksi yläkäsitteiksi, kuten seuraava esimerkki osoittaa.
Oletetaan, että on olemassa yllä määriteltyjen luokkien lisäksi piirreluokka Student
,
joka kuvaa opiskelijoita. Halutaan määritellä, että kurssiassarit ovat myös opiskelijoita
sen lisäksi, että he ovat työntekijöitä. Tämä onnistuu helposti:
class TeachingAssistant extends Employee with Student {
// Nyt assarit ovat kaikkia seuraavista tyypeistä: TeachingAssistant, Student,
// PersonAtAalto, Employee. (Assareilla on PersonAtAalto-piirreluokan ominaisuudet
// vain kertaalleen, vaikka tuohon yläkäsitteeseen viekin kaksi eri "polkua".)
}
Jos liitettäviä piirreluokkia on vielä enemmän, ne voi luetella näin:
class X extends MyTrait1 with MyTrait2 with MyTrait3 with MyTrait4 with Etc
Liitettävillä piirreluokilla ei ole pakko olla yhteistä yläkäsitettä (kuten PersonAtAalto
on Employee
lle ja Student
ille oheisessa esimerkissä), vaan luokkaan voi liittää
toisiinsa muuten liittymättömiäkin piirreluokkia.
o1
-pakkauksen mallinnustyökaluja (ja FlappyBug vielä kerran)
Kurssikirjastossa o1
on piirreluokka HasVelocity
, joka kuvaa yleisellä tasolla
sellaisia asioita, joilla on sijainti ja nopeus kaksiulotteisessa koordinaatistossa.
Joitakin tuollaisia asioita olemmekin ohjelmissamme mallintaneet, joskin hyödyntämättä
tätä piirreluokkaa. Tutkitaan, mitä välineitä se meille tarjoaa.
Tutustu HasVelocity
piirreluokan dokumentaatioon.
Huomaa, että sillä on yläkäsitteenä toinen piirreluokka HasPos
; tutustu siihenkin. Jos et tutustunut luvun 3.5 vapaaehtoisessa tehtävässä
nopeutta kuvaavaan Velocity
, aloita lukemalla siitä.
Mieti, miten voisit lisätä FlappyBug-projektin luokille Bug
ja Obstacle
HasVelocity
-piirteen ja hyödyntää sitä luokkien toteutuksessa. Tee tuo muutos
luokkiin. Lue halutessasi vinkit alta.
Vinkkejä Obstacle
-luokkaan:
- Alkuun tarvitaan tietysti
extends
-lisäys. - Luokan on toteutettava
HasVelocity
-piirreluokan abstrakti metodivelocity
. Lisää se. Esteen vauhti on x-suunnassa vakio ja y-suunnassa nolla. - Huomaa
HasVelocity
n tarjoama metodinextPos
. Hyödynnä sitä metodinapproach
toteutuksessa.
Vinkkejä Bug
-luokkaan:
- Tähänkin tarvitaan
extends
-määrittely javelocity
-metodi. - Tässäkin voit käyttää
nextPos
ia ötökkää liikuttavan koodin yksinkertaistamiseen (fall
-metodissa taimove
-apumetodissa, jos teit sellaisen aiemmin, joskin sen voi ehkä nyt poistaa tarpeettomana).- Ehkä keksit myös tavan yksinkertaistaa
esteluokan
touches
-metodia samalla?
- Ehkä keksit myös tavan yksinkertaistaa
esteluokan
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Ilmentymämuuttujat piirreluokassa
Piirreluokassa voi olla myös ilmentymämuuttujia.
trait Supertype { val magicNumber = 42 val text: String }defined trait Supertype class Subtype extends Supertype { val text = "value of 'text' for all Subtype instances" }defined class Subtype
Nuo muuttujat löytyvät kaikilta alakäsitteiden ilmentymiltä:
val myObject: Supertype = new SubtypemyObject: Supertype = Subtype@714aadf7 myObject.magicNumberres14: Int = 42 myObject.textres15: String = value of 'text' for all Subtype instances
Piirreluokat vs. Java-kielen rajapintaluokat
Sanalla rajapinta (interface) on muun muassa seuraavat (toisiinsa liittyvät) merkitykset:
ohjelman osan eli esimerkiksi luokan "julkisivu", jonka kautta tuota osaa voi käyttää (luku 3.1);
eräissä kielissä (erityisesti: Javassa) esiintyvä rakenne, josta voidaan käyttää myös nimeä rajapintaluokka ja joka muistuttaa Scalan piirreluokkia:
// Tämä on Javaa, ei Scalaa. interface Shape { /* jne. */ } class Circle implements Shape { /* jne. */ }
Javan rajapintaluokista Scalan piirreluokat eroavat ensisijaisesti siten, että piirreluokissa voi olla myös ilmentymäkohtaisia muuttujia, kun taas rajapintaluokissa ei voi.
Etukäteen määriteltyjä ilmentymiä
Piirreluokat ja yksittäisoliot
Yksittäisolioon voi liittää piirreluokan extends
-sanalla aivan kuin luokkaankin.
Itse asiassa olet jo tehnytkin niin. Scalan luokka App
on nimittäin piirreluokka,
joka kuvaa sovellusohjelman käsitettä ja tarjoaa erinäisiä sovelluksen käynnistämiseen
liittyviä palveluita. Kun olet kirjoittanut ohjelman käynnistysolioon extends App
olet
siis määritellyt, että kyseiseen yksittäisolioon liittyy App
-piirre. Tai toisin sanoen:
kyseinen yksittäisolio on erikoistapaus yläkäsitteestä App
.
Myös seuraavassa esimerkkiohjelmassa liitetään piirteitä yksittäisolioihin. Samalla tehtävä toimii esimerkkinä tilanteesta, jossa kaikki tietyn tyypin ilmentymät ovat etukäteen tiedossa, eikä ole tarkoituskaan että kukaan voisi niitä määritellä lisää.
Verityyppitehtävän paluu
Luvun 4.4 mallinnettiin verityyppejä. Käytimme ABO-luokittelun ja Rhesus-luokittelun yhdistelmää, ja kuvasimme kunkin verityypin merkkijonon ja totuusarvon yhdistelmänä tähän tapaan:
val myBlood = new BloodType("AB", true)myBlood: o1.blood.BloodType = AB+ val yourBlood = new BloodType("A", true)yourBlood: o1.blood.BloodType = A+ myBlood.canDonateTo(yourBlood)res16: Boolean = false
Aiemmassa versiossamme:
BloodType
-käsite, joka tarkoitti aina
yhdistelmää ABO-verityypistä ja Rhesus-verityypistä.Kuitenkaan näitä kahta ei aina välttämättä käytetä yhdessä, ja verityyppiluokitteluja on olemassa muitakin. Lisäksi:
Tehdään ohjelmastamme joustavampi kuvaamalla ABO- ja Rhesus-luokittelutavat erikseen. Samalla otamme mallinnuksen välineeksi piirreluokat, joilla kuvaamme näitä eri luokitteluja.
Tarkastellaan aluksi Rhesus-luokittelua. Kuvataan sen mukaisia verityyppejä
piirreluokalla Rhesus
:
trait Rhesus {
val isPositive: Boolean
def isNegative = !this.isPositive
def canDonateTo(recipient: Rhesus) = this.isNegative || this == recipient
def canReceiveFrom(donor: Rhesus) = donor.canDonateTo(this)
}
isPositive
. Millä tahansa
oliolla, joka kuvaa Rhesus
-luokittelun mukaista verityyppiä,
tulee olla tämä muuttuja ja sillä jokin arvo.Rhesus
-tyyppi määrittelee metodit, jotka ovat Rhesus-luokittelun
mukaisilla verityypeillä (käytännössä siis molemmilla kahdesta
tyypistä).Koska verityyppejä on pieni määrä ja ne ovat etukäteen tiedossa, voimme luontevasti
kuvata kunkin yksittäisen verityypin yksittäisolioilla. Rhesus-luokittelussa ryhmiä
on kaksi: positiivista verityyppiä kuvatkoon yksittäisolio RhPlus
ja negatiivista
RhMinus
:.
object RhPlus extends Rhesus {
val isPositive = true
override def toString = "+"
}
object RhMinus extends Rhesus {
val isPositive = false
override def toString = "-"
}
Rhesus
-piirre, eli ne
ovat yksittäistapauksia Rhesus
-yläkäsitteestä. Niillä on kaikki
piirreluokan metodit.Nyt verityyppien Rhesus-luokittelun mukaisia yhteensopivuuksia voi tutkia näitä olioita käyttäen:
RhPlus.canDonateTo(RhMinus)res17: Boolean = false RhMinus.canDonateTo(RhPlus)res18: Boolean = true RhMinus.canDonateTo(RhMinus)res19: Boolean = true RhMinus.isPositiveres20: Boolean = false
Yleisemmin sanoen: tähän tapaan voimme mallintaa käsitteen (piirreluokan), josta on tarjolla rajattu joukko nimettyjä, etukäteen tunnettuja ja kaikki tapaukset kattavia ilmentymiä (yksittäisoliot).
Vastaava voisi olla tarjolla myös ABO-luokittelulle: piirreluokka ABO
ja yksittäisoliot
A
, B
, AB
ja O
. Ja muillekin veriryhmäluokitteluille, jos niitä haluaisimme mallintaa.
Seuraavissa vapaaehtoisissa osioissa toteutat ABO-luokittelun samalla periaatteella ja voit tutustua siihen, miten näitä luokitteluja voi yhdistellä.
ABO-verityypeille oma piirreluokka
Projektissa Subtypes on pakkaus o1.blood
. Sen tiedostossa BloodType.scala
on
äskeinen Rhesus
-koodi. Lisää samaan tiedostoon myös piirreluokka ABO
sekä
yksittäisoliot A
, B
, AB
ja O
; nuo neljä ovat ainoat oliot, joilla on tuo
piirre.
ABO
-piirreluokassa on oltava:
- Abstrakti muuttuja
antigens
, johon on tarkoitus tallentaa merkkijonona kyseisen verityypin sisältämät antigeenit (esim. "A" tai "AB"). - Metodit
canDonateTo
jacanReceiveFrom
, jotka toimivat vastaavasti kuin luvun 4.4BloodType
-luokan ja äskeisenRhesus
-piirreluokan samannimiset metodit. Nämä metodit tosin tarkastelevat tyyppien yhteensopivuutta ainoastaan ABO-antigeenien perusteella eivätkä huomioi Rhesus-tekijää.
Yksittäisolioilla A
, B
, AB
ja O
on oltava:
- Konkreettinen arvo
antigens
-muuttujalle siten, että se luettelee mitkä antigeenit kyseiseen veriryhmään liittyvät: "A", "B", "AB" tai "". (Viimeinen on siis tyhjä merkkijono.) toString
-metodi, joka palauttaa veriryhmän nimen. Nimi on sama kuinantigens
, paitsi että O-veriryhmän nimi on "O".
Jos et muista, miten veriryhmien yhteensopivuus määrittyy, kertaa luvusta 4.4 ja joko omasta ratkaisustasi aiempaan tehtävään tai esimerkkiratkaisusta.
Tiedostosta BloodTest.scala
löytyy testiohjelma, jolla voit kokeilla koodiasi.
Testikoodia on tosin kommentoitu ulos, jotta et saisi turhia virheilmoituksia; poista
kommentit, kun olet valmis testaamaan.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Entä luokittelujen yhdisteleminen?
Äsken erottelimme aiemmin yhdessä käytetyt veriluokittelut. Tarkoitus olisi, että niitä voisi käyttää joustavasti erikseen tai yhdistellä keskenään tai muihin luokitteluihin. Miten tuo yhteiskäyttö nyt sitten voisi käydä?
Yksi tapa on toteutettu luokaksi ABORh
, joka löytyy (ulos kommentoituna)
BloodType.scala
sta sekä tästä:
class ABORh(val abo: ABO, val rhesus: Rhesus) {
def canDonateTo(recipient: ABORh) = this.abo.canDonateTo(recipient.abo) && this.rhesus.canDonateTo(recipient.rhesus)
def canReceiveFrom(donor: ABORh) = donor.canDonateTo(this)
override def toString = this.abo.toString + this.rhesus.toString
}
BloodType
-luokkaamme.
Se on rakennettu ABO- ja Rhesus-luokittelut yhdistämällä.ABO
- ja Rhesus
-tyyppisiä arvoja eli käytännössä
niitä yksittäisolioita, joita olemme määritelleet.Luokan sulkeminen
Äskeinen ohjelmamme perustui ajatukseen, että Rhesus
-piirreluokan toteuttaa vain
pari tiettyä oliota, jotka olemme etukäteen koodiin kirjanneet, ja ABO
-piirreluokan
vastaavasti neljä. Nuo samaan tiedostoon kirjaamamme yksittäisoliot kattavat kaikki
mahdolliset tapaukset; Rhesus
- ja ABO
-tietotyyppien käyttäjä voi luottaa siihen, että
kunhan hän huomioi nuo arvot, niin kaikki on kunnossa. Piirreluokat Rhesus
ja ABO
on
ikään kuin suljettu umpeen: niiden käyttäjä ei voi keksiä niille uusia ilmentymiä eikä ole
tarkoituskaan.
Voimme kirjata asian Scalana:
sealed trait Rhesus {
// ...
}
sealed
piirreluokan määrittelyn alussa kertoo, että tuota
piirrettä ei voi liittää mihinkään muihin luokkiin tai olioihin
kuin niihin, jotka on määritelty samassa tiedostossa.Voit lisätä sanan sealed
piirreluokkien Rhesus
ja ABO
määrittelyihin.
(Tuossa suljimme piirreluokan. Myös tavallisen luokan voi sulkea, mistä puhutaan luvussa 7.3.)
Luetelmatyypeistä
Kuten näit, suljetun yläkäsitteen ja yksittäisolioiden yhdistelmällä voi kuvata tyyppejä, joilla on rajattu, tunnettu, kattava joukko mahdollisia arvoja. Samankaltaiseen tavoitteeseen käytetään monissa ohjelmointikielissä luetelmatyyppejä (enumerated type). Myös Scalalla voi määritellä luetelmatyyppejä, mutta tässä luvussa esitetty vaihtoehto on usein kätevämpi.
Syvemmälle Scalan tyyppijärjestelmään
(Tämä lisätehtävä on äskeistä verityyppitehtävää haastavampi ja edellyttää omatoimista opiskelua kurssimateriaalin ulkopuolelta. Se sopii tässä kohden kurssia lähinnä ennestään ohjelmoinnin parissa harrastuneille. Aloittelijoiden kannattaa ohittaa tämä ja muutkin voivat.)
Saimme jo kuvattua Rhesus
-verityypit, ABO
-verityypit ja niiden yhdistelmän
ABORh
. Noille piirreluokillemme oli yhteistä canDonateTo
-metodi, ja
canReceiveFrom
-metodi (jolla oli jopa identtinen toteutus) sekä ajatus siitä,
että kyseessä on eräs verityyppiluokittelu.
Määritellään yläkäsite BloodType
, joka kuvaa noiden kolmen piirreluokan
yhteistä yläkäsitettä. Rhesus
, ABO
ja ABORh
ovat kaikki BloodType
jä.
Tässä ensimmäinen hahmotelma:
trait BloodType {
def canDonateTo(recipient: BloodType): Boolean
}
sealed trait Rhesus extends BloodType { /* etc. */ }
sealed trait ABO extends BloodType { /* etc. */ }
sealed trait ABORh extends BloodType { /* etc. */ }
Rhesus
, ABO
ja ABORh
.Tutki, mikä virhetilanne syntyy. Pohdi, miksi niin käy.
Tarkastele sitten tätä versiota:
trait BloodType[ThisSystem] {
def canDonateTo(recipient: ThisSystem): Boolean
}
Lue tyyppiparametreista Scalan dokumentaatiosta
ja muualta. Mitä alatyyppeihin Rhesus
, ABO
ja ABORh
pitää nyt lisätä,
jotta ne toimisivat yhteen tyyppiparametrillisen BloodType
-yläkäsitteen kanssa?
Tee muutokset koodiin. (Tälle tehtävälle ei ole automaattista tarkistinta.
Arvioi itse oman ratkaisusi toimivuus.)
Äskeisestä puuttui vielä toinen metodi. Tarkoitus olisi poistaa identtinen
canReceiveFrom
alatyypeistä ja toteuttaa se kertaalleen BloodType
-piirreluokassa.
Näin:
trait BloodType[ThisSystem] {
def canDonateTo(recipient: ThisSystem): Boolean
def canReceiveFrom(donor: ThisSystem) = donor.canDonateTo(this)
}
Tuo versiopa ei enää toimikaan. Tutki, mikä virhetilanne syntyy. Pohdi, miksi niin käy.
Tässä versio, jossa molemmat metodit on kelvollisesti määritelty:
trait BloodType[ThisSystem <: BloodType[ThisSystem]] {
this: ThisSystem =>
def canDonateTo(recipient: ThisSystem): Boolean
def canReceiveFrom(donor: ThisSystem) = donor.canDonateTo(this)
}
ThisSystem
tulee olla jokin tyypin BloodType[ThisSystem]
alatyyppi.this
viittaa tässä
piirreluokassa ThisSystem
-tyyppiseen olioon.Selvitä, miten koodi ratkaisee edellisen version ongelman. Etsi mainituista käsitteistä lisätietoja netistä tarpeen mukaan.
Yhteenvetoa
- Piirreluokka on tavallista luokkaa muistuttava rakenne, jolla voi mallintaa käsitteen ja joka määrittelee tietotyypin.
- Piirreluokassa voi määritellä ilmentymämuuttujia ja metodeita, jotka ovat yhteisiä useammalle luokalle tai yksittäisoliolle. Piirreluokkien avulla voi näin kuvata yläkäsitteitä.
- Luokkaan tai yksittäisolioon voi liittää piirreluokan (tai useita), jolloin sen oma määrittely täydentyy piirreluokan määrittelyllä.
- Piirreluokan metodit voivat olla abstrakteja. Tämä tarkoittaa sitä, että metodille ei määritellä yleistä toteutusta piirreluokassa vaan alakäsitteille erikseen. Kuitenkin metodin olemassaolo taataan kaikissa olioissa, joilla on kyseinen piirre. Myös muuttuja voi olla abstrakti vastaavasti.
- Jos yläkäsitteellä on ennalta tunnettu joukko ilmentymiä, voi ne kuvata yksittäisolioina, joilla on sama yläkäsite.
- Lukuun liittyviä termejä sanastosivulla: piirreluokka eli piirre, abstrakti metodi, abstrakti muuttuja; staattinen tyyppi, dynaaminen tyyppi; DRY; abstraktio; suljettu luokka.
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!
Kierrokset 1–13 ja niihin liittyvät tehtävät ja viikkokoosteet on laatinut Juha Sorva.
Kierrokset 14–20 on laatinut Otto Seppälä. Ne eivät ole julki syksyllä, mutta julkaistaan ennen kuin määräajat lähestyvät.
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 Riku Autio, Jaakko Kantojärvi, Teemu Lehtinen, 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 ovat suunnitelleet Juha Sorva ja Teemu Sirkiä. Niiden teknisen toteutuksen ovat tehneet Teemu Sirkiä ja Riku Autio käyttäen Teemun toteuttamia Jsvee- ja Kelmu-työkaluja.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset on laatinut Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.
Opetustapa, jossa 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+ on luotu Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Pääkehittäjänä toimii tällä hetkellä Jaakko Kantojärvi, jonka lisäksi järjestelmää kehittävät useat tietotekniikan ja informaatioverkostojen opiskelijat.
Kurssin tämänhetkinen henkilökunta on kerrottu luvussa 1.1.
Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.
shapes
-puskurin alkioiden tyypiksi?