Luku 7.3: Käsitteitä sukupuussa: piirreluokat
Johdanto: tasokuvioita
Oletetaan, että olemme laatimassa sovellusta, 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:
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
end Circle
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
end Rectangle
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:
@main def shapeTest() =
val shapes = Buffer[?????]()
shapes += Circle(10)
shapes += Rectangle(10, 100)
shapes += Circle(5)
var sumOfAreas = 0.0
for current <- shapes do
sumOfAreas += current.area
println("Pinta-alojen summa on: " + sumOfAreas)
end shapeTest
Ja edelliseen liittyvästi: Mikä on 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:
@main def shapeTest() =
val shapes = Buffer(Circle(10), Rectangle(10, 100), Circle(5))
var sumOfAreas = 0.0
for current <- shapes do
sumOfAreas += current.area
println("Pinta-alojen summa on: " + sumOfAreas)
end shapeTest
Tämä on sinänsä täysin mahdollinen käsky. Scala-kieleen kuuluu
tyyppipäättely (luku 1.8), ja koska alkioiksi on merkitty
ympyröitä ja suorakaide, niin Scala-työkalut päättelevät, että
tässä ei luoda "puskuria, jossa on ympyröitä" eikä "puskuria,
jossa on suorakaiteita" vaan "puskuri, jossa on sekalaisia
olioita". (Tarkemmin sanoen tyyppi on Buffer[AnyRef]
. Siihen,
mikä AnyRef
on, palaamme luvussa 7.5.)
Jos puskurin alkioina on mielivaltaisia olioita, voi muuttuja
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. Siksi on mainiota,
että kääntäjä osaa kyseenalaistaa metodikutsun current.area
. Kun puskurimme sisältää
"mitä vaan olioita", ei area
-metodia tosiaan pidäkään mennä noin vain kutsumaan
puskurin alkioille.
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 äskeinen 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ä tarkemmasta käsitteestä (kuten ympyrä) että sen yläkäsitteestä (kuten kuvio). Voimme myös ajatella, että:
"Tämä ohjelma laskee kuvioiden kokonaispinta-alan."
"Metodi
area
on kaikilla kuvioilla.""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."
Voimme myös esittää tämän ajatuksen tietokoneohjelmassa, kuten kohta näkyy.
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
end Shape
Tässä määritellään piirreluokka (lyhyemmin vain piirre;
trait) nimeltä 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".
Piirreluokan määrittelyssä käytetään avainsanaa trait
sanan
class
sijaan, mutta muuten se muistuttaa kovasti tutunlaisia
luokkamäärittelyjä.
Määritellään: kaikilla kuvioilla on tällainen isBiggerThan
-metodi,
jolla voi verrata kuvioiden pinta-aloja keskenään.
Parametrin tyyppi on Shape
, eli tälle metodille annetaan
parametriksi juuri sellainen kuvio-olio, jollaisia tämä
piirreluokka kuvaa. Huomaat: piirreluokkaa voi käyttää
tietotyyppinä siinä missä tavallisiakin luokkia.
Määritellään: kaikilla kuvioilla on tällainen area
-metodi
pinta-alan laskemiseen. Mutta: määrittelemmekin vain metodin
nimen, parametrit (joita ei tässä tapauksessa ole) ja
paluuarvon tyypin (Double
kaksoispisteen perässä).
Sen sijaan varsinaista toteutusta eli pinta-alan laskemistapaa tälle metodille ei ole määritelty lainkaan! Sanomme: kyseessä on abstrakti metodi (abstract method).
Loppumerkki on tässäkin vapaaehtoinen. Se on tapana kirjoittaa ainakin silloin, jos piirreluokan koodissa on tyhjiä rivejä.
Toisin kuin nähdynlaisiin tavallisiin luokkiin, piirreluokkiin voi määritellä
abstrakteja, toteutuksettomia metodeita. Esimerkiksi Shape
-piirreluokassa ilmoitetaan,
että minkä tahansa kuvion pinta-alan voi 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
Yläkäsitettä kuvaavalle piirreluokalle määritellään alakäsite
avainsanalla extends
.
Määrittelyn voi tässä lukea vaikkapa näin: "Luokka Circle
laajentaa piirreluokan Shape
kuvaamaa tietotyyppiä." Tai:
"Luokka Circle
perii (inherits) Shape
-piirteen."
Ajatus on joka tapaussa se, että kaikki Circle
-tyyppiset oliot
ovat paitsi Circle
jä myös Shape
-tyyppisiä; niillä siis on
myös Shape
-piirreluokan kuvaamat ominaisuudet.
Luokassa voidaan tarjota toteutukset piirreluokan abstrakteille metodeille. Esimerkiksi tässä määritellään, että ympyrä on sellainen kuvio, jonka pinta-ala lasketaan π * r2, ja suorakaide on sellainen kuvio, jonka pinta-ala lasketaan sivujen kertolaskulla.
Huomaa: Ympyröillä ja suorakaiteilla on extends Shape
-määrittelyn vuoksi myös
isBiggerThan
-metodi, vaikka emme sitä 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ä Shape()
? Ja mitä silloin syntyy? Kokeillaan:
Shape()-- Error ...
Ei ole olemassa Shape
-olioita, jotka olisivat "vaan kuvioita", eikä sellaista voi
luoda. 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 = 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
Toisin kuin tällä kurssilla yleensä käytetyille
metodeille isInstanceOf
-metodille annetaan
tyyppiparametri hakasulkeissa.
Kysytään 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]-- Warning: |ympyra.isInstanceOf[Rectangle] |^^^^^^ |this will always yield false since type o1.shapes.Circle and class Rectangle are unrelated res2: Boolean = false
Piirreluokka alkioiden tyyppinä
Vector(Circle(1), Circle(2))res3: Vector[o1.shapes.Circle] = Vector(o1.shapes.Circle@e17571, o1.shapes.Circle@1e56bea) Vector(Circle(1), Rectangle(2, 3))res4: Vector[o1.shapes.Shape] = Vector(o1.shapes.Circle@876228, o1.shapes.Rectangle@3d619a)
Kun kokoelmassa on vain ympyröitä, päätellään taulukon alkioiden
tyypiksi Circle
.
Kun kokoelmassa on myös suorakaiteita, päätellään taulukon alkioiden
tyypiksi näiden yhteinen yläkäsite eli piirreluokka 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 mahdollisten
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
end Shape
Nuo kaksi area
-kutsua isBiggerThan
-metodissa ovat mahdollisia
siksi, että kaikilla Shape
illa on area
-määrittely. Abstraktikin
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 niiden piirreluokalta Shape
perimä 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ä
tietyntyyppiseltä oliolta löytyvät tietyt metodit.
shapeTest
kin toimii
Myös shapeTest
-esimerkkiohjelma toimii nyt alkuperäisessä muodossaan:
@main def shapeTest() =
val shapes = Buffer(Circle(10), Rectangle(10, 100), Circle(5))
var sumOfAreas = 0.0
for current <- shapes do
sumOfAreas += current.area
println("Pinta-alojen summa on: " + sumOfAreas)
end shapeTest
Puskurin alkioiden tyypiksi tulee päätellyksi Shape
.
Jos siltä tuntuu, voisimme kirjata tyypin koodiin itsekin
kirjoittamalla tähän Buffer[Shape]
.
Tämäkin area
-kutsu on nyt mahdollinen, koska current
on tyyppiä Shape
ja piirreluokka takaa, että kaikilla
Shape
-olioilla on toteutus area
-metodille.
Piirreluokat ja yksittäisoliot
Yksittäisolio voi periä piirteen extends
-sanalla aivan kuin luokkakin voi. Tällainen
olio on yksittäinen erikoistapaus piirreluokan kuvaamasta käsitteestä. Jos piirreluokassa
on abstrakteja metodeja, olion on toteutettava ne.
object SingularShape extends Shape:
def area = 51 // toteuttaa abstraktin metodin
val description = "non-Euclidian" // tämän yksittäisolion lisämuuttuja
Itse asiassa olet jo tehnytkin vastaavasti. Scalan App
on nimittäin piirreluokka,
joka kuvaa sovellusohjelman käsitettä. Kun olet kirjoittanut ohjelman käynnistysolioon
extends App
olet siis määritellyt, että kyseinen yksittäisolio perii App
-piirteen.
Tai toisin sanoen: kyseinen yksittäisolio on erikoistapaus yläkäsitteestä App
.
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ä asiaan on syytä hieman tarkentaa.
Staattisesti tyypitetyissä kielissä kuten Scalassa 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.
Metodikutsusta jokuOlio.jokuMetodi()
seuraa käännösaikainen virheilmoitus, ellei
jokuOlio
n staattinen tyyppi 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 staattinen tyyppikin.
Ero staattisen ja dynaamisen tyypin väliltä löytyy esimerkiksi seuraavasta ohjelmanpätkästä:
var test: Shape = Rectangle(10, 20)
println(test.area)
test = Circle(10)
println(test.area)
val selected = readLine("Haluatko ympyrän? Sano 'joo', jos haluat, muuten tulee neliö. ")
if selected == "joo" then
test = Circle(readLine("Säde: ").toInt)
else
val sivu = readLine("Sivu: ").toInt
test = Rectangle(sivu, sivu)
println(test.area)
Muuttuja test
määritellään tässä Shape
-tyyppiseksi, jolloin
siihen voi tallentaa viittauksen mihin tahansa olioon, jonka
tyyppi perii Shape
n.
Kunkin lausekkeen test
staattinen tyyppi on siis Shape
.
Kaikki esimerkin metodikutsut test.area
ovat sallittuja,
koska Shape
-tyypille on määritelty area
-metodi.
Kuitenkin 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 = Rectangle(5, 10)test1: o1.shapes.Rectangle = o1.shapes.Rectangle@38c8ed
test1
-muuttujan (staattinen) tyyppi tulee päätellyksi siihen sijoitettavan lausekkeen
tyypin perusteella. Se on siis Rectangle
, eikä muuttujaan voi sijoittaa viittausta
ympyräolioon. Virheilmoitus on suhteellisen selväsanainen:
test1 = Circle(10)-- Error: |test1 = Circle(10) | ^^^^^^^^^^ | Found: o1.shapes.Circle | Required: o1.shapes.Rectangle
Mutta jos määräämme muuttujalle erikseen tyypiksi Shape
, niin staattinen tyyppi on
"laajempi" kuin arvon dynaaminen tyyppi:
var test2: Shape = Rectangle(5, 10)test2: o1.shapes.Shape = o1.shapes.Rectangle@bdee1c
Tällaiseen muuttujaan voi sijoittaa myös ympyräviittauksen:
test2 = 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
end Shape
Tässä 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.
Sen sijaan tässä metodissa ei voisi ryhtyä vertailemaan kuvioita
käyttäen vaikkapa lauseketta 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.
Sama pätee muuten 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 Shape
-tyyppiseen muuttujaan tallennettuna vaikkapa viittaus
Circle
-olioon, ja haluamme kutsua olion radius
-metodia? Tai mitä jos haluamme tehdä
valinnan ohjelma-ajon aikana sen perusteella, viittaako Shape
-tyyppinen muuttuja
tietyllä hetkellä ympyrään vai johonkin muuhun kuvioon?
match
-käsky (luku 4.3) tulee avuksi. Se tekee valinnan nimenomaan dynaamisen tyypin
perusteella. Näin:
var someShape: Shape = 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
Tapauksessa, jossa someShape
ssa on Circle
-tyyppinen
arvo, tuo arvo tulee tallennetuksi someCircle
-nimiseen
muuttujaan. someCircle
n staattinen tyyppi on Circle
.
Ilmaisun case _
voi lukea "missä tahansa muussa
tapauksessa" (luku 4.4).
Tiedoksi kiinnostuneille: huonompi tapa
Toinen mutta lähes aina huonompi mahdollisuus on asInstanceOf
-metodi, joka on kaikilla Scala-olioilla. Huomaa tyyppiparametri ja
paluuarvon staattinen tyyppi:
someShape.asInstanceOf[Circle]res5: Circle = Circle@1de2de2 someShape.radius-- Error: ... value radius is not a member of o1.shapes.Shape
Varoitus! Tämä aiheuttaa ajonaikaisen virhetilanteen, jos
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 — O1’llä ei koskaan.
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
.
Luokan tulee periä 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 = RightTriangle(3.0, 4.0)triangle: o1.shapes.RightTriangle = o1.shapes.RightTriangle@18bcb2d triangle.hypotenuseres6: Double = 5.0 triangle.areares7: Double = 6.0 Circle(3).isBiggerThan(triangle)res8: Boolean = true triangle.isBiggerThan(Rectangle(7, 5))res9: Boolean = false
isBiggerThan
-metodia ei tässä tehtävässä saa eikä kannata kirjoittaa RightTriangle
-luokkaan. Kolmioillakin se on, koska ne perivät sen Shape
-piirreluokasta.
Osa 2/2: piirinlaskemismetodi kaikille kuvioille
Lisää Traits-moduulin
Shape
-piirreluokkaan uusi abstrakti, parametriton metodiperimeter
, joka laskee ja palauttaa kuvion piirin (eli reunaviivan kokonaispituuden)Double
-tyyppisenä lukuna.Lisättyäsi abstraktin metodin
Shape.scala
an huomaat IntelliJ’n 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
. (Moduulissa on myös luokka nimeltäSquare
, johon palaamme myöhemmin.) Käyttöesimerkkejä:
triangle.perimeterres10: Double = 12.0 Circle(5).perimeterres11: Double = 31.41592653589793 Rectangle(2, 5).perimeterres12: Double = 14.0
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 osat ovat yleiskäyttöisempiä. 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 helpompi ylläpitää. Lisäksi uusien alakäsitteiden lisääminen on helppoa. Esimerkiksi kun 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
Yksi piirreluokka voi periä toisen. 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.
end PersonAtAalto
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.
end PersonAtAalto
Nyt kun määritellään luokka TeachingAssistant
, joka perii Employee
n, 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.
end TeachingAssistant
Tietotyyppien "sukupuita" kutsutaan tyyppihierarkioiksi (type hierarchy).
Useita välittömiä yläkäsitteitä
Esimerkissämme TeachingAssistant
-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, Student:
// Nyt assarit ovat kaikkia seuraavista: TeachingAssistant,
// Student, PersonAtAalto, Employee. (Assareilla on
// PersonAtAalto-piirteen ominaisuudet vain kertaalleen,
// vaikka tuohon yläkäsitteeseen viekin kaksi eri "polkua".)
end TeachingAssistant
Jos piirreluokkia peritään vielä enemmän, ne voi luetella näin:
class X extends MyTrait1, MyTrait2, MyTrait3, MyTrait4
Noin luetelluilla piirreluokilla ei ole pakko olla yhteistä yläkäsitettä (vaikka
esimerkissämme PersonAtAalto
sellainen Employee
lle ja Student
ille olikin),
vaan luokka voi periä toisiinsa muuten liittymättömiäkin piirreluokkia.
Pilkkujen sijaan voi käyttää with
-sanaa. Tee kummin vain, mutta molemmat tyylit
on hyvä tunnistaa:
class TeachingAssistant extends Employee with Student class X extends MyTrait1 with MyTrait2 with MyTrait3 with MyTrait4
Yläkäsitteet ja metodien korvaaminen
Kakkoskierrokselta alkaen olemme käyttäneet sanaa override
metoditoteutusten korvaamiseen
uusilla. Erityisen usein olemme käyttäneet sitä:
toString
-metodeissa (luku 2.5): laatimammetoString
-toteutukset korvaavat oletusarvoisen toteutuksen (joka tuottaa kuvauksia kuteno1.shapes.Square@ecfb83
).View
-olioiden tapahtumankäsittelijöissä (kutenonClick
; luku 3.1):View
-luokan tarjoamat oletustoteutukset reagoivat tapahtumiin olemalla jouten, mutta voimme korvata ne sovellukseen sopivilla reaktioilla.
Metodeita voi korvata tyyppihierarkioissa yleisemminkin. Tehdään kokeeksi muutama miniluokka:
trait PiirreX:
def tervehdys = "Terveisiä lähettää PiirreX"
class Luokka1 extends PiirreX
class Luokka2 extends PiirreX:
override def tervehdys = "Terveisiä lähettää Luokka2"
Kokeillaan REPLissä:
Luokka1().tervehdysres13: String = Terveisiä lähettää PiirreX Luokka2().tervehdysres14: String = Terveisiä lähettää Luokka2
Luokka1
saa tervehdys
-metodinsa PiirreX
’ltä.
Luokka2
korvaa tuon metodin omalla versiollaan.
Scalassa sana override
on kirjoitettava metodin määrittelyyn aina, kun korvaa
yläkäsitteen metoditoteutuksen.
Miksi override
-pakko? Koska se on suolainen.
Kun kirjoitat koodiin override
, niin kuittaat tietäväsi, että
"tässä korvaan yläkäsitteelle määritellyn toteutuksen toisella".
Ellei override
-sanaa vaadittaisi, saattaisit hyvinkin sattumalta
ja huomaamattasi antaa metodille sellaisen nimen, joka on jo
muussa käytössä jossakin yläkäsitteessä, mistä voisi seurata
erikoisiakin bugeja.
Samoin kuin esimerkiksi staattinen tyypitys
myös override
-merkintä on käytäntö, joka pienentää virheiden
riskiä. Tällaista ohjelmointikielen piirrettä, joka hankaloittaa
huonon koodin kirjoittamista, sanotaan joskus "syntaktiseksi
suolaksi" (vrt. yleisempi termi syntaktinen sokeri).
Lisäetu on, että korvaaminen tulee näin dokumentoitua myös koodin lukijalle.
Lisätään tyyppihierarkiaamme toinen piirreluokka ja pari luokkaa:
trait PiirreY extends PiirreX:
override def tervehdys = "Terveisiä lähettää PiirreY"
class Luokka3 extends PiirreY
class Luokka4 extends PiirreY:
override def tervehdys = "Terveisiä lähettää Luokka4"
Toinen piirreluokkamme siis perii ensimmäisen, ja uudet luokat edelleen perivät tämän toisen piirreluokan.
Kokeillaan luokkia:
Luokka3().tervehdysres15: String = Terveisiä lähettää PiirreY Luokka4().tervehdysres16: String = Terveisiä lähettää Luokka4
Luokka3
ei määrittele omaa toteutusta tervehdysmetodille.
Se perii tuon metodin välittömältä yläkäsitteeltään PiirreY
(joka korvaa PiirreX
’n ylempänä hierarkiassa määrittelemän
toteutuksen).
Luokka4
määrittelee metoditoteutuksen, joka korvaa toteutukset
kummastakin yläkäsitteestä PiirreY
ja PiirreX
.
Tutkitaan vielä vähän:
var olio: PiirreX = Luokka1()olio: PiirreX = Luokka1@6d293b41 olio.tervehdysres17: String = Terveisiä lähettää PiirreX olio = Luokka4()olio: PiirreX = Luokka4@dc6c5ca olio.tervehdysres18: String = Terveisiä lähettää Luokka4
Huomaa: Muuttujan staattinen tyyppi on PiirreX
. Sen arvon
dynaaminen tyyppi on Luokka4
.
tervehdys
-metodia voi kutsua mille tahansa lausekkeelle, jonka
staattinen tyyppi on PiirreX
(tai jokin PiirreX
’n alatyyppi),
so. mille tahansa oliolle, jolla taatusti on tämä metodi. Se, mitä
kutsuttaessa tapahtuu, puolestaan riippuu siitä, mikä on viestin
vastaanottavan olion dynaaminen tyyppi. Tässä siis suoritetaan
nimenomaan Luokka4
-tyyppisille oliolle määritelty korvaava
testimetodi, vaikka muuttujan tyyppi on PiirreX
.
Yläkäsitteeseen viittaminen: super
Tehdään vielä yksi kokeiluluokka:
class Luokka5 extends PiirreY:
override def tervehdys = super.tervehdys + ", ja niihin yhtyy Luokka5"
Avainsanalla super
voi viitata yläkäsitteen osaksi tehtyyn määrittelyyn.
Tässä kutsutaan yläkäsitteeltä perittyä versiota tervehdys
-metodista.
Luokka5
-tyyppisen olion tervehdys
-metodi siis ensin kutsuu yläkäsitteen
PiirreY
versiota metodista ja saamansa merkkijonon perään Luokka5
’lle
ominaisen tekstinpätkän. Esimerkki alla.
Luokka5().tervehdysres19: String = Terveisiä lähettää PiirreY, ja niihin yhtyy Luokka5
Pikkuharjoitus: super
and override
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
Ilmentymämuuttuja voi olla abstrakti kuten metodikin.
Nuo muuttujat periytyvät alakäsitteille ja löytyvät niiden ilmentymiltä:
val myObject: Supertype = Subtype()myObject: Supertype = Subtype@714aadf7 myObject.magicNumberres20: Int = 42 myObject.textres21: String = value of 'text' for all Subtype instances
Luontiparametreja piirreluokassa
Esimerkki: terveysammattilaisia
Laaditaan kokeeksi piirreluokka kuvaamaan terveysalan ammattilaisia:
trait MedicalPro
Tuossa sivussa näkyy tyyppihierarkia, jonka pian määrittelemme.
Keskitytään aluksi vain yhteen MedicalPro
n alakäsitteistä. Ensihoitajasta (paramedic)
mallinnetaan, onko hän töissä ambulanssissa (vai sairaalan päivystyksessä):
class Paramedic(val inAmbulance: Boolean) extends MedicalPro
Entä jos haluamme nyt kuvata, että kaikilla MedicalPro
-olioilla on työnantaja, joka
on tallessa String
-tyyppisessä muuttujassa ja joka saa arvonsa luontiparametrista
oliota luodessa?
Voimme lisätä piirreluokkaan luontiparametrin ja samalla muuttujan (val
) ihan
kuin tavalliseen luokkaankin.
trait MedicalPro(val employer: String)
MedicalPro
-olioita ei luoda suoraan vaan alatyyppiensä kautta. Miten siis tuolle
luontiparametrille välitetään arvo? Ja miten työnantajatieto pitäisi huomioida
Paramedic
-luokassa?
Sanotaan, että haluamme luoda Paramedic
-olioita seuraavasti, antaen
luontiparametriksi ensin työnantajan ja sitten ambulanssi-Boolean
in.
val ensihoitaja = Paramedic("Helsingin kaupunki", true)ensihoitaja: Paramedic = Paramedic@61c98b6c
Pannaan Paramedic
-luokkamme toimimaan noin. Tehdään samalla (esimerkin vuoksi) oletus,
että ensihoitajien työnantaja on aina jokin kaupunki.
class Paramedic(city: String, val inAmbulance: Boolean) extends MedicalPro(city)
Lisäsimme luokalle luontiparametrin. Huomaa, että sen edessä
ei ole val
-sanaa sen merkiksi, että luokalla on city
-niminen
ilmentymämuuttuja. Tarkoitus on, että tämän city
-luontiparametrin
arvo päätyy employer
-ilmentymämuuttujaan, joka on määritelty
MedicalPro
-piirreluokassa.
inAmbulance
-luontiparametri ja sitä vastaava ilmentymämuuttuja
meillä oli jo edellisessä Paramedic
-luokan versiossa. Tässä ei
ole mitään uutta.
Koodin kiinnostavin kohta on tämä: kun alakäsitteestä
luodaan ilmentymä, tehdään myös yläkäsitteelle määritellyt
alustustoimenpiteet. Alakäsitteen määrittelystä usein
välitetään luontiparametreja yläkäsitteen ohjelmakoodin
hoidettaviksi. Esimerkiksi tässä määritellään, että kun
Paramedic
-oliota luodaan, tehdään samat alustukset kuin mille
vain MedicalPro
-oliolle ja että MedicalPro
lle annetaan
luontiparametriksi ensimmäinen uuden Paramedic
-olion
saamista kahdesta luontiparametrista. (Ks. animaatio alla.)
Esimerkki jatkuu
Syvennetään tyyppihierarkiaamme. Lisätään piirreluokka Doctor
. Emme tässä
pikkuesimerkissä mallinna lääkäreistä muita tietoja kuin MedicalPro
-olioista
yleensäkin, joten tämä riittää:
trait Doctor extends MedicalPro
Yksinkertainen mallimme jakaa lääkärit kahteen ryhmään, yleislääkäreihin ja
erikoislääkäreihin, joista jälkimmäisiä on erilaisia. Piirreluokka Specialist
ilmoittaa, että kustakin erikoislääkäristä kirjataan hänen erikoisalansa:
trait Specialist(val specialization: String) extends Doctor
Tarkoitus olisi, että yleislääkäreitä kuvaavaa luokkaa voisi käyttää näin:
val yleislaakari = GeneralPractitioner("InstaCare Hospital")yleislaakari: GeneralPractitioner = GeneralPractitioner@4df03572
Tässä ensimmäinen yritys toteutukseksi:
class GeneralPractitioner(employer: String) extends Doctor // ei toimi
Perusidea tuossa on ihan oikein: yleislääkäri periytyy lääkäristä, ja lääkärihän on jo
edellä periytetty MedicalPro
sta. Jotain silti puuttuu. Käännösaikainen virheilmoitus
antaa vinkin:
class GeneralPractitioner(employer: String) extends Doctor-- Error: |class GeneralPractitioner(employer: String) extends Doctor | ^ | parameterized trait MedicalPro is indirectly implemented, | needs to be implemented directly so that arguments can be passed
Toisin sanoen: koska korkeammalla tyyppihierarkiassa oleva piirreluokka MedicalPro
vaatii luontiparametrin, on meidän sellainen sille annettava. Vika korjaantuu mainitsemalla
tuo yläkäsite erikseen. Tämä toimii:
class GeneralPractitioner(employer: String) extends MedicalPro(employer), Doctor
Ilmoitamme, että MedicalPro
-yläkäsitteelle välitetään
parametriksi juuri se työnantajamerkkijono, jonka luotavalle
GeneralPractitioner
-oliolle annettiin.
Huomaa tässäkin, että luontiparametrin edessä ei lue
val
, vaan employer
-niminen ilmentymämuuttuja on
jo määritelty MedicalPro
-piirreluokassa. Tässä kyse
on vain GeneralPractitioner
in luontiparametrin nimestä,
jonka voimme valita vapaasti; employer
käy hyvin.
Tekemättä on vielä luokka Neurologist
, jonka on tarkoitus kuvata hermostoon
erikoistuneita lääkäreitä. Sen toivomme toimivan näin:
val laakari = Neurologist("Chicago Grace Hospital")laakari: Neurologist = Neurologist@4f13e602 laakari.specializationres22: String = neurology
Kerrataan vielä piirreluokat, jotka meillä jo on:
trait MedicalPro(val employer: String)
trait Doctor extends MedicalPro
trait Specialist(val specialization: String) extends Doctor
Näillä eväillä voimme kirjoittaa Neurologist
-luokan:
class Neurologist(employer: String) extends MedicalPro(employer), Specialist("neurology")
Välitämme työnantajan luontiparametriksi
MedicalPro
lle kuten edelläkin teimme.
Myös Specialist
-piirreluokka vaatii merkkijonon
luontiparametriksi. Välitämme sille tietyn
tekstin, jonka haluamme tulevan erikoisalaksi
kaikille Neurologist
-olioille.
Doctor
-piirreluokkakin kyllä on Neurologist
-luokan yläkäsite. Sitä ei tuossa
extends
-rimpsussa tarvitse erikseen mainita, koska Doctor
on Specialist
in
yläkäsite eikä Doctor
ille tarvitse välittää luontiparametreja.
Halutessasi löydät tämän esimerkin koodin Traits-moduulista.
Tehtävä: erilaisia viestejä
Traits-moduulin pakkauksessa o1.messages
on muutama yksinkertainen luokka, jotka
(leikisti) kuvaavat verkkopalvelun käyttäjien toisilleen lähettämiä viestejä. Viestejä
on muutamaa eri tyyppiä:
DirectMessage
on tietylle vastaanottajalle lähetetty viesti.Post
on viesti, joka ei ole kohdennettu kenellekään yksittäiselle ihmiselle.Comment
on viesti, joka on lähetetty vastauksena johonkin alkuperäiseenPost
iin.Message
on kaikkien viestien yläkäsite; se on piirreluokka. Kaikille viesteille on yhteistä se, että niillä oncontent
-niminen ilmentymämuuttuja, jossa on viestin sisältö merkkijonona.Ja onpa vielä määritelty myös käsite
Reply
, piirreluokka sekin.Reply
kuvaa viestejä, jotka ovat vastauksia johonkin. Kuten oheisesta kaaviosta näkyy,Comment
in on tarkoitus olla eräänlainenReply
, joskin tämä on vielä annetussa koodissa määrittelemättä.
Tehtäväsi on tutustua noiden pikkuluokkien koodiin ja täydentää sitä seuraavasti:
Täydennä
Comment
-luokanextends
-rimpsua niin, että luokka perii myösReply
n. Muista, ettäReply
vaatii luontiparametrin; voit välittää sille senPost
-olion, johon kommentti vastaa ja jonkaComment
saaoriginal
-parametrinaan.Lisää kaikille
Message
-olioille tieto siitä, onko kyseinen viesti julkinen vai ei. Tee se lisäämälläMessage
-piirreluokan otsikkoriville toinen luontiparametri ja ilmentymämuuttuja:val isPublic: Boolean
. Muokkaa sitten luokatDirectMessage
,Post
jaComment
yhteensopiviksi uusitunMessage
n kanssa. Tarkemmin sanoen:Kohdennetut viestit eivät ole julkisia: välitä
DirectMessage
staMessage
-piirreluokalle jälkimmäiseksi luontiparametriksi literaalifalse
.Post
voi näkyä julkisesti tai olla näkymättä. LisääPost
-luokalle toinen luontiparametri, joka on tyyppiäBoolean
. Välitä tuon luontiparametrin arvo eteenpäinMessage
-luokalle.Parametrin nimi voi olla
isPublic
tai jotain muuta. Huomaa, että eteen ei tuleval
, koska tarkoitus ei ole määritellä uutta ilmentymämuuttujaa.
Comment
voi myös olla julkinen tai ei-julkinen. Lisää sille (kolmas) luontiparametri vastaavasti.
Samassa tiedostossa on myös pieni testiohjelma. Kun olet tehnyt pyydetyt muutokset, voit poistaa kommenttimerkit testiohjelman ympäriltä ja tarkistaa tulosteen.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Kokoava koodinlukutehtävä
Seuraava vähän hölmö ohjelma kokoaa yhteen edellä esiteltyjä asioita. Voi käyttää sitä tietojesi tarkistukseen. Jos ymmärrät ohjelman toiminnan yksityiskohtaisesti(!), niin ymmärrät myös keskeisimmät piirreluokkiin ja tyyppihierarkioihin liittyvät ohjelmarakenteet. Tehtävä on vapaaehtoinen.
Autoilutarina
Lue seuraava koodi. Mieti perusteellisesti, mitkä tekstit se tulostaa ja missä järjestyksessä. Kirjoita mieluiten tuloste itsellesi muistiin!
@main def driveAbout() =
val car = Car()
car.receivePassenger(Schoolkid())
car.receivePassenger(ChemicalEngineer("C. Chemist"))
car.receivePassenger(MechanicalEngineer("M. Machine"))
car.receivePassenger(ElectricalEngineer("E. Electra"))
car.receivePassenger(ComputerScientist("C.S. Student"))
car.start()
class Car:
private val passengers = Buffer[Passenger]()
def receivePassenger(passenger: Passenger) =
passenger.sitDown()
this.passengers += passenger
def start() =
println("(The car won't start.)")
for passenger <- this.passengers do
passenger.remark()
end Car
trait Passenger(val name: String):
def sitDown() =
println(this.name + " finds a seat.")
def speak(sentence: String) =
println(this.name + ": " + sentence)
def diagnosis: String
def remark() =
this.speak(this.diagnosis)
end Passenger
trait Student extends Passenger:
def diagnosis = "No clue what's wrong."
class Schoolkid extends Passenger("Anonymous pupil"), Student
trait TechStudent extends Student:
override def remark() =
super.remark()
this.speak("Clear as day.")
class ChemicalEngineer(name: String) extends TechStudent, Passenger(name):
override def diagnosis = "It's the wrong octane. Next time, I'll do the refueling."
class MechanicalEngineer(name: String) extends TechStudent, Passenger(name):
override def diagnosis = "Nothing wrong with the gas. It must be the pistons."
override def speak(sentence: String) =
super.speak(sentence.replace(".", "!"))
class ElectricalEngineer(name: String) extends TechStudent, Passenger(name):
override def sitDown() =
println(this.name + " claims a front seat.")
override def diagnosis = "Hogwash. The spark plugs are faulty."
class ComputerScientist(name: String) extends TechStudent, Passenger(name):
override def remark() =
this.speak(super.diagnosis)
this.speak(this.diagnosis)
override def diagnosis = "Let's all get out of the car, close the doors, reopen, and try again."
Kävithän koodin ajatuksella läpi? Kirjoititko odottamasi tulosteen muistiin?
Avaa nyt Traits-moduuli ja aja pakkauksesta o1.cruising
löytyvä ohjelma
(jonka koodi on yllä). Vastasiko tuloste täsmälleen sitä, mitä odotit? Jos ei,
selvitä mistä erot johtuivat. Voit käyttää debuggeria
apuna.
Ohjelmointiharjoitus: oikeushenkilöitä
Tehtävänanto
Tutustu pakkauksen o1.legal
dokumentaatioon Traits-moduulissa. Se kuvaa useita luokkia,
joilla voi mallintaa oikeusjuttuja sekä erilaisia oikeushenkilöitä
(legal entity tai legal person), jotka toimivat oikeusjutuissa asianomistajina ja
vastaajina.
Toteuta luokat moduulin sisältämiin Scala-tiedostoihin. (Paikat niille on merkitty koodiin kommentein.)
Luokkia on monta, mutta ne ovat yksinkertaisia. Tehtävän keskiössä ovat näiden käsitteiden väliset suhteet. Ne on esitetty kuvana alla.
Tämä tehtävä palautetaan kahdessa osassa:
Osa 1: Toteuta
CourtCase
,Entity
ja kaikkiEntity
n alatyypit paitsi tyyppihierarkianJuridicalPerson
-haara.Osa 2: Toteuta myös
JuridicalPerson
alatyyppeineen.
Osa 1/2: Suositeltu etenemisjärjestys ja muita vinkkejä
Selaa aluksi ainakin tyyppien
CourtCase
,Entity
,NaturalPerson
jaJuridicalPerson
dokumentaatio, niin saat kokonaiskuvan luokista.Laadi luokka
CourtCase
. Huomaa, että oikeusjuttuun liittyy kaksiEntity
-piirreluokan tyyppistä muuttujaa: kyseiset oliot ovat jonkinlaisia oikeushenkilöitä, muttaCourtCase
ei ota kantaa siihen, millaisia.Laadi piirreluokka
Entity
omaan tiedostoonsa.Laadi
NaturalPerson
samannimiseen tiedostoon.Yläkäsite on ilmoitettu
extends
-sanalla dokumentaatiossa.Dokumentaatio kertoo, mitkä metodit periytyvät yläkäsitteiltä ja mitkä ovat kyseiselle alakäsitteelle uusia. Kunkin Scaladoc-sivun alkupäässä on kohta, jonka avulla voi valita (Filter), mitkä metodit sivulla näytetään. Napsauttamalla tuon kohdan auki löydät Inherited-osion, jonka nappuloilla voit säädellä, näkyvätkö sivulla myös yläkäsitteiltä periytyvät metodit. Kokeile.
Laadi luokka
FullCapacityPerson
(eli täysvaltainen luonnollinen henkilö).Varsinkin, kun luokkamme ovat pieniä, voimme hyvin tehdä niin, että sijoitamme
NaturalPerson
in alatyypit samaan tiedostoon tuon yläkäsitteensä kanssa.Huomaa dokumenttisivun alusta: tämä luokka merkitään alakäsitteeksi sekä
Entity
lle ettäNaturalPerson
ille. Vain jälkimmäinen riittäisi muuten, muttaEntity
llekin on välitettävä luontiparametri. (Tarve on vastaava kuinMedicalPro
- jaMessage
-ohjelmissa ylempänä.)Huomaa myös, että ilmentymämuuttuja nimelle on jo määritelty
Entity
-piirreluokassa, joten sitäval
-sanaa ei pidä toistaa täällä. (Tältäkin osin tilanne on sama kuin edeltävissä esimerkissä.)
Ota esiin
Restriction.scala
, jota käytetään kohta apuna vajaavaltaisten henkilöiden kuvaamisessa. PiirreluokkaRestriction
on jo tehty, samoin sen erikoistapauksena yksittäisolioIllness
. Lisää vastaava yksittäisolioUnderage
.Toteuta
ReducedCapacityPerson
.Tälläkin kertaa luontiparametreja pitää välittää kahdelle eri yläkäsitteelle.
Jos olet toteuttanut aiemmat metodit oikein, niin tämän pitäisi toimia
kind
-metodin toteutuksena:override def kind = super.kind + " with " + this.restriction
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Osa 2/2: Suositeltu etenemisjärjestys ja muita vinkkejä
Toteuta
JuridicalPerson
. Yksikin rivi riittää (koska lisämetodeita ei tarvita).Toteuta
HumanOrganization
jaGeographicalFeature
.Yläkäsitteen abstraktiksi jättämän parametrittoman
def
in voi korvata myös muuttujamäärittelyllä. MääritteleHumanOrganization
iincontact
-muuttuja jaGeographicalFeature
enkind
-muuttuja.
Lisää
def
in toteuttamisesta muuttujallaParametrittoman abstraktin metodin voi tosiaan toteuttaa muuttujallakin, kuten esim.
contact
-muuttujalle tuossa ehdotettiin. Oleellista on, että lausekkeellasomeOrganization.contact
onNaturalPerson
-tyyppinen arvo. On luokan käyttäjän näkökulmasta merkityksetöntä, onko kyseessäval
-muuttuja vai vaikutukseton, parametriton metodi, joka palauttaa aina saman arvon. (Lisää aiheesta Wikipedia-artikkelissa uniform access principle sekä Philip Schwartzin opetusmateriaalissa.)Sinun ei tarvitse vaivautua kirjoittamaan
Nation
-,Municipality
- jaCorporation
-luokkia, joissa ei ole mitään uutta asiaa. Voit yksinkertaisesti poistaa kommentit annettujen toteutusten ympäriltä. Jos olet laatinut ohjelman oikein, näiden luokkien pitäisi toimia sellaisinaan.Toteuta luokka
Group
.Ryhmillä ei ole tässä ohjelmassa keskenään erilaisia nimiä. Välitä
Entity
-piirreluokalle luontiparametriksi dokumentaation mukainen merkkijonoliteraali.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Huutokaupat uusiksi piirreluokilla
Seuraava ohjelmointitehtävä on jatkoa aiemmille kauppapaikka-aiheisille tehtäville. Itse tehtävä on vapaaehtoinen, mutta suosittelen, että vähintäänkin tutustut siihen, mihin tehtävässä pyritään.
Luvussa 5.1 kehitit luokan FixedPriceSale
, ehkä myös luokat DutchAuction
ja
EnglishAuction
. Nämä luokat kuvaavat eri tavoin myytäviä esineitä. Luvun 5.5
esimerkissä puolestaan laadimme luokan AuctionHouse
, joka edusti sellaisia
kauppapaikkoja, joissa kaikki esineet ovat huudettavissa perinteiseen EnglishAuction
-tyyliin.
Jos teit mainitut aiemmat tehtävät, voit käyttää omia ratkaisujasi pohjana seuraavalle tehtävälle. Jos et, voit käyttää esimerkkiratkaisuja (FixedPriceSale, DutchAuction, EnglishAuction).
Uudistus luokkarakenteeseen
Aiemmissa tehtävissä esiintyneet luokat suhtautuvat toisiinsa suurin piirtein näin:
Toisin sanoen: AuctionHouse
ssa on englantilaistyyppisiä huutokauppoja. Luokat
FixedPriceSale
ja DutchAuction
ovat täysin irrallisia.
Tässä tehtävässä refaktoroit aiemmin laadittua koodia. Refaktoroinnin
tavoite on parempi laatu: muokkaat FixedPriceSale
-, EnglishAuction
- ja DutchAuction
-luokkia niin, että niitä kaikkia voi käyttää yhdessä. Samalla vähennät turhaa toistoa
koodissa. Refaktoroinnin työkaluna toimivat tässä tapauksessa piirreluokat.
Tarkoitus olisi rakentaa tällainen versio:
Keskeisin uudistus on, että piirreluokka ItemForSale
on yläkäsite eri tavoin myytäville
esineille. Kun näin on, voimme käyttää tätä yläkäsitettä, kun toteutamme yleishyödyllisemmän
AuctionHouse
-luokan. Lisäksi InstantPurchase
-piirre kokoaa yhteen vakiohintaisten
esineiden ja hollantilaishuutokauppojen yhteisiä ominaisuuksia.
Toteuta uudistus
Toteuta ItemForSale
, EnglishAuction
, InstantPurchase
,
FixedPriceSale
, DutchAuction
ja AuctionHouse
vastaamaan
AuctionHouse2-moduulin Scaladoc-dokumentaatiota.
Ohjeita ja vinkkejä:
Luokat kannattanee toteuttaa yllä luetellussa järjestyksessä.
Katso dokumentaatiosta tarkasti, mitkä metodit ovat abstrakteja. Katso myös, mitkä metodit periytyvät kullekin luokalle sen yläkäsitteiltä.
Älä nytkään toista
val
-sanaa alatyypin luontiparametreissa, jos kyseinen muuttuja on jo piirreluokassa määritelty. Älä siis käytä esimerkiksidescription
-muuttujan kohdallaval
-sanaa muualla kuin piirreluokassaItemForSale
. Alakäsitteillekin sopii kyllä kirjoittaa tuon niminen luontiparametri.Tässäkin kauppapaikka-aiheisessa tehtävässä voit käyttää annettua käyttöliittymää kokeillaksesi tärkeimpien metodien toimivuutta. Käynnistysolion
o1.auctionhouse.gui.TestApp
ohjelmakoodista tosin tulee aluksi virheilmoituksia, mutta ne kaikkoavat, kunhan saat oman toteutuksesi käynnistyskuntoon.Ainoa
AuctionHouse
-luokaan tarvittava muutos on, että korvaatEnglishAuction
in yleisemmällä käsitteellä.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Tehtävän tehtyäsi voit ihalla, miten tyyppihierarkia muutti vanhat irralliset ja toisteiset koodit kauniiksi käsitteelliseksi malliksi, jossa kustakin käsitteestä on määritelty vain juuri ne muutamat asiat, jotka erottavat sen sukulaiskäsitteistä.
Toinen valinnainen lisätehtävä
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
olemme jo 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.6
vapaaehtoisessa tehtävässä nopeutta kuvaavaan Velocity
,
aloita lukemalla siitä.
Mieti, miten voisit lisätä FlappyBug-ohjelman 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?
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
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. Piirreluokkien avulla voi näin kuvata yläkäsitteitä.
Luokka voi periä piirreluokan (tai useita), jolloin luokan oma määrittely täydentyy piirreluokan määrittelyllä.
Piirreluokista ja tavallisista luokista voi muodostaa tietotyyppien "sukupuita", tyyppihierarkioita.
Piirreluokan metodin voi määritellä abstraktiksi. Tämä tarkoittaa, että metodille ei määritellä yleistä toteutusta piirreluokassa vaan alakäsitteille erikseen. Kuitenkin metodin olemassaolo taataan kaikissa olioissa, joilla on kyseinen piirre.
Alakäsite voi korvata yläkäsitteen metodin alakäsitteelle ominaisella toteutuksella.
Lukuun liittyviä termejä sanastosivulla: piirreluokka eli piirre, abstrakti metodi, abstrakti muuttuja; tyyppihierarkia; staattinen tyyppi, dynaaminen tyyppi; DRY; abstraktio; korvata (metodi).
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
Kiitos autoilutarinan pohjana olleen vitsin keksijälle, kuka sitten onkin.
Mitä ?????-merkinnän kohdalle pitäisi kirjoittaa? Mikä voidaan mainita
shapes
-puskurin alkioiden tyypiksi?