Tämä kurssi on jo päättynyt.

Kurssin viimeisimmän version löydät täältä: O1: 2024

Luku 7.5: Periytyminen

Tästä sivusta:

Pääkysymyksiä: Miten määrittelen alakäsitteen luokalle, joka ei ole piirreluokka? Miten Scalan valmiit luokat muodostavat tyyppihierarkian? Kuulin jonkun puhuvan "abstraktista luokasta"; mikä se on?

Mitä käsitellään? Luokan periytyminen toisesta. Scala APIn tyyppihierarkioita. Abstraktit luokat.

Mitä tehdään? Luettavaa on jonkin verran, mistä osa on ihan vapaaehtoista mutta ehkä kiinnostavaa. Ohjelmointitehtäviä on yksi pieni.

Suuntaa antava työläysarvio:? Ehkä tunti.

Pistearvo: B20.

Oheismoduulit: Traits.

../_images/person04.png

Johdanto

Edellisissä luvuissa on opittu, miten yläkäsitteitä voi mallintaa piirreluokilla. Tämä luku täydentää edellisiä: tutustumme siihen, miten myös tavallisen luokan voi merkitä toisten luokkien yläkäsitteeksi. Tätä perinteistä olio-ohjelmointitekniikkaa kutsutaan periytymiseksi tai perinnäksi (inheritance).

Lisää tasokuvioita

Luvussa 7.3 määrittelimme piirreluokan Shape ja sille toteuttavan luokan Rectangle:

trait Shape:
  def isBiggerThan(another: Shape) = this.area > another.area
  def area: Double
class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape:
  def area = this.sideLength * this.anotherSideLength

Mitä jos haluamme lisätä ohjelman tyyppivalikoimaan neliöt: sellaiset suorakaiteet, joiden jokaisen sivun pituus on aina täsmälleen sama kuin muidenkin sivujen? Neliöolion voisi luoda käskyllä kuten Square(10).

Yksi tapa olisi tietysti luoda Square-luokka, johon piirre Shape liitetään näin:

class Square(val sideLength: Double) extends Shape:
  def area = this.sideLength * this.sideLength

Kalvamaan jää, että koodissa on nyt selvästi toistoa: neliön pinta-alanlaskemisalgoritmi on aivan sama kuin suorakaiteenkin; sattuu vain olemaan niin, että sivut ovat saman mittaiset. Toteutus ei ilahduta käsitteellisen mallinnuksen kannaltakaan, sillä se asettaa neliöt suorakaiteiden rinnalle Shape-tyypin alakäsitteeksi. Ihmisinä miellämme, että neliöt ovat erikoistapaus suorakaiteista: kukin neliö on myös suorakaide (ja kuvio).

Ongelma ratkeaa helposti: voidaan määritellä, että neliö on suorakaiteen alakäsite. Alla kuvana esitetyn käsitehierarkian voi muodostaa, vaikka Rectangle onkin tavallinen luokka eikä piirreluokka.

../_images/inheritance_shape_square.png

Ali- ja yliluokat

Tehdään pieni lisäys Rectangle-luokkaan:

open class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape:
  def area = this.sideLength * this.anotherSideLength

Vaikka kyseessä on tavallinen luokka (class), josta voi luoda ilmentymiä, niin tämä luokka on avoin. Kirjoittamalla avainsanan open ilmoitamme, että Rectangle saa vapaasti toimia yliluokkana (superclass) mille vain toisille luokille.

Squaresta tehdään Rectanglen aliluokka (subclass):

class Square(size: Double) extends Rectangle(size, size)

Käytämme jälleen extends-sanaa. Tällä kertaa sen perässä on mainittu tavallinen luokka eikä piirreluokka. Sanotaan: luokka Square perii (inherits) luokan Rectangle. Square-oliot ovat nyt myös Rectangle-tyyppisiä.

Luokalla Square on vain yksi konstruktoriparametri, joka kertoo kunkin sivun mitan.

Kun aliluokasta luodaan ilmentymä, tehdään myös yliluokassa määritellyt alustustoimenpiteet. Aliluokka voi välittää konstruktoriparametreja yliluokalleen; idea on sama kuin piirreluokille luvussa 7.3. Tässä määritellään, että kun Square-oliota luodaan, tehdään samat alustustoimenpiteet kuin Rectangle-oliolle, kuitenkin niin, että molemmiksi suorakaiteiden konstruktoriparametreiksi (eli molemmiksi sivunpituuksiksi) laitetaan neliöolion saaman konstruktoriparametrin arvo.

Kuinka välttämätön tuo open on?

On syynsä sille, miksi ei ole hyvä ajatus periyttää aliluokkia ihan mistä tahansa luokasta, joka ei ole moiseen tarkoitettu. Nuo syyt on havaittu todellisiksi isoissa ohjelmaprojekteissa eivätkä tule kunnolla esille tällaisella alkeiskurssilla. Pientä osviittaa niistä saa lisälukemistosta tämän luvun lopussa.

Scalassa siis luokka merkitään avoimeksi open-sanalla. Näin ohjelmoija ilmoittaa, että kyseinen luokka on suunniteltu käytettäväksi periytymisessä ja sille sopii määritellä aliluokkia vapaasti. Ellei luokkaa noin avaa, ei sille pidä määritellä aliluokkia muualla kuin samassa kooditiedostossa. open-sanan poisjättö ei teknisesti tee mahdottomaksi määritellä aliluokkia muuallakin, mutta Scala-kääntäjä varoittaa moisesta epäilyttävästä koodista, ja tuollaista periytymistä onkin parempi välttää. (Vrt. sulkeminen sealed-sanalla, joka täysin estää suorien alakäsitteiden määrittelemisen muissa tiedostoissa; luku 7.4.)

Joissakin poikkeustilanteissa on perusteltua laatia aliluokka sellaisellekin toisaalla määritellylle luokalle, joka ei ole avoin. Kääntäjän varoitusilmoituksen voi tällöin poistaa antamalla käskyn import scala.language.adhocExtensions. O1-kurssilla tälle ei ole tarvetta.

Periytyminen vs. piirreluokat

Luokan määritteleminen toisen luokan aliluokaksi näyttää kovasti samanlaiselta kuin luvussa 7.3 nähty piirreluokan liittäminen luokkaan. Samankaltaisesta asiasta onkin kyse.

Erojakin on; vertaillaan. Tässä ensin piirreluokkien ominaisuuksia käyttäen esimerkkinä piirreluokkaa Shape, joka kuvaa Rectangle-luokan yläkäsitteen:

Kun käytetään piirreluokkaa:

Esimerkki

Yläkäsitettä kuvaa piirreluokka.

trait Shape

Alakäsitteeseen liitetään tuo piirre.

class Rectangle extends Shape

Piirreluokassa saa olla abstrakteja metodeita ja muuttujia.

def area: Double

Piirreluokasta ei voi luoda suoraan ilmentymää.

Pelkkä Shape() ei toimi.

Luokkaan voi liittää useita piirreluokkia.

class X extends Piirre1, Piirre2, Piirre3 toimii.

Piirreluokka ei voi välittää konstruktoriparametreja ylityyp(e)illeen.

trait Piirre1 extends Piirre2(parametrit) ei toimi.

Ja tässä vastaavasti periytymisen ominaisuuksia. Esimerkkinä on yliluokka Rectangle, joka kuvaa Square-luokan yläkäsitteen:

Kun käytetään periytymistä:

Esimerkki

Yläkäsitettä kuvaa tavallinen luokka (joka on hyvä määritellä avoimeksi, jos tarkoitettu periytymiseen).

open class Rectangle

Aliluokka periytyy yliluokasta.

class Square extends Rectangle

Tavallisessa luokassa ei saa olla abstrakteja metodeja tai muuttujia. (Paitsi, että... lisää aiheesta kohta.)

Rectanglen kaikilla metodeilla on toteutus.

Yliluokasta voi luoda ilmentymän suoraan.

Rectangle(...) toimii.

Luokalla saa (mm. Scalassa) olla vain yksi välitön yliluokka.

class X extends Yli1, Yli2   ei toimi. (Mutta
class X extends Yli1, Piirre1, Piirre2 on sallittu.)

Mikä vain tavallinen luokka (class) saa välittää konstruktoriparametreja ylityyp(e)illeen.

class Square(size: Int) extends Rectangle(size, size) toimii.

On monia tilanteita, joissa kumpi tahansa näistä tekniikoista kelpaa.

Ohjelmointiharjoitus: esineitä esineiden sisällä

Johdanto

Oletetaan, että laaditaan ohjelmaa, jossa on tarkoitus kuvata erilaisia esineitä. Käytössämme on yksinkertainen luokka Item:

open class Item(val name: String) :
  override def toString = this.name

Otetaan tässä pienessä ohjelmointitehtävässä tavoitteeksi, että tällaisten "tavallisten esineiden" lisäksi ohjelmassamme olisi sellaisia esineitä, joiden sisällä voi olla toisia esineitä. Esimerkiksi laukun sisällä voisi olla kirja ja laatikko, joista laatikon sisällä olisi sormus.

Tehtävänanto

Laadi Itemille aliluokka Container, joka kuvaa säkkien ja laatikoiden kaltaisia esineitä, jotka voivat sisältää toisia esineitä:

  • Tällaisilla säiliöesineillä on nimi kuten muillakin esineillä.

  • Lisäksi niillä on addContent-metodi, jolla sisältöä lisätään.

  • Sekä contents-metodi, joka palauttaa lisätyt esineet vektorissa.

  • Container-esineiden toString-metodi poikkeaa tavallisten esineiden vastaavasta.

Laatimasi luokan tulisi toimia tähän tapaan:

val container1 = Container("box")container1: o1.items.Container = box containing 0 item(s)
container1.addContent(Item("ring"))
container1res0: o1.items.Container = box containing 1 item(s)
val container2 = Container("bag")container2: o1.items.Container = bag containing 0 item(s)
container2.addContent(Item("book"))
container2.addContent(container1)
container2res1: o1.items.Container = bag containing 2 item(s)
container1.contentsres2: Vector[o1.items.Item] = Vector(ring)
container2.contentsres3: Vector[o1.items.Item] = Vector(book, box containing 1 item(s))

toStringin palautusarvossa sisällettyjen esineiden lukumäärään otetaan mukaan vain välittömästi sisällä olevat. Esimerkissämme laukun sisällä on siis kaksi esinettä, vaikka laukun sisältämän laatikon sisällä onkin vielä sormus.

(Miten saataisiin kaikki "sisällön sisällötkin"? Palataan siihen luvussa 12.2.)

Ohjeita ja vinkkejä

  • Traits-moduulin pakkauksesta o1.items löytyy tuo Item-luokka, josta tosin puuttuu alusta open. Lisää se.

  • Samasta pakkauksesta löytyy myös alku Container-luokalle sinun täydennettäväksesi.

  • Älä unohda, että yliluokalle Item on välitettävä konstruktoriparametri.

  • Älä määrittele ilmentymämuuttujaa name uudelleen val-sanaa käyttäen luokassa Container. Tuo muuttuja on jo määritelty yliluokassa. Container-luokan konstruktoriparametrin nimi voi silti hyvin olla name.

  • Huomaa, että tässä tehtävässä korvaat (override) yliluokan Item toteutuksen toString-metodille etkä Scala-olioiden oletustoteutusta kuten aiemmissa ohjelmissa.

  • Osaatko toteuttaa aliluokan toString-metodin niin, että kutsut sen sisältä yliluokan toString-metodia sen sijaan, että katsoisit esineen nimen suoraan? (Tämä ei ole välttämätöntä.)

  • Älä lisää luokkaan muita julkisia jäseniä kuin pyydetyt. Yksityisiä jäseniä voit lisätä harkintasi mukaan.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Abstraktit luokat

Eroa piirreluokkien ja periytymisen välillä hämärtää se, että on mahdollista määritellä niin sanottuja abstrakteja luokkia (abstract class). Abstraktit luokat muistuttavat monin tavoin piirreluokkia, mutta eroavat niistä tietyin tavoin.

O1-kurssin kannalta abstraktit luokat eivät ole keskeisiä; suosimme ohjelmissamme piirreluokkia. Ohjelmointiopintojesi edetessä opit valitsemaan abstraktien luokkien ja piirreluokkien välillä itsenäisemmin. Nyt riittää tietää yleissivistävästi, että abstrakteja luokkia on olemassa, koska ne eivät ole harvinaisuus Scalassa tai ohjelmointikielissä muutenkaan. Kaikissa ohjelmointikielissä ei ole näitä kahta käsitettä erikseen.

Katsotaan esimerkki. Luvun 7.3 ohjelmassa meillä oli piirreluokka Entity:

trait Entity(val name: String):
  def contact: NaturalPerson
  def kind: String
  override def toString = s"$name ($kind)"

Kaksi metodeista on abstrakteja. Tällaisia alatyyppien toteutettavaksi jätettyjä metodeita ei voi ihan tavallisessa luokassa olla, mutta seuraava vaihtoehtoinen määrittely on mahdollinen:

abstract class Entity(val name: String):
  def contact: NaturalPerson
  def kind: String
  override def toString = s"$name ($kind)"

Sana abstract alussa tekee luokasta abstraktin. Tällaiseen luokkaan saa kirjata abstrakteja metodeita kuten piirreluokkaankin. Ja samoin kuin piirreluokasta ei voi luoda suoraan ilmentymiä, ei voi abstraktista luokastakaan. Abstraktit luokat ovat avoimia ilman erillistä mainintaa, eli open-sanaa ei lisäksi tarvita.

Luokkaa, joka ei ole abstrakti eikä piirreluokka, voi vertailun vuoksi sanoa konkreettiseksi luokaksi (concrete class).

Abstrakti luokka muistuttaa eräin tavoin piirreluokkaa ja eräin tavoin konkreettista yliluokkaa. Verrataan kootusti:

Piirreluokka

Abstrakti
yliluokka
Konkreettinen
yliluokka

Voiko se sisältää abstrakteja metodeja?

Voi.

Voi.

Ei voi.

Voiko siitä luoda suoraan ilmentymiä?

Ei voi.

Ei voi.

Voi.

Voiko se välittää konstruktoriparametreja yläkäsitteilleen?

Ei voi.

Voi.

Voi.

Voiko sellaisia luetella yläkäsitteinä useita (extends-sanan perässä)?

Voi.

Ei voi.

Ei voi.

Käyttäisinkö piirreluokkaa vai abstraktia luokkaa, noin yleisemmin?

Nyrkkisääntö: Ellei ole erityistä syytä käyttää abstraktia luokkaa, käytä piirreluokkaa, koska niitä voi liittää luokkiin joustavammin.

Valinta piirreluokan ja abstraktin yliluokan välillä voi tuntua hankalalta. O1-kurssilla yleensä annetaan luokkatason spesifikaatiot valmiina, ja valinnat on tältä osin tehty puolestasi. Aihe on ajankohtaisempi kurssilla Ohjelmointistudio 2. Vähän lisää aiheesta löytyy myös Kirjoja ja linkkejä -sivulla mainituista kirjoissa Scala Cookbook ja Programming in Scala.

Luokkahierarkiat Scala APIssa

Kuten piirreluokkia, myös periytymistä voi käyttää käsitehierarkioiden muodostamiseen. Eräitä esimerkkejä hierarkioista löytyy Scala APIsta. Katsotaan muutama.

Käyttöliittymäelementtien hierarkia

Pakkaus scala.swing tarjoaa luokkia, jotka kuvaavat käyttöliittymien rakennuspalikoita eli GUI-elementtejä. Alla on osittain kuvattu se hierarkia, jonka nämä luokat muodostavat.

../_images/inheritance_swing.png

Osa näistä pakkauksen scala.swing luokista on piirreluokkia, osa tavallisia luokkia. Kukin alakäsite saa ominaisuuksia yläkäsitteittensä kautta. Ylin käsite UIElement määrittelee kaikille elementeille yhteisiä ominaisuuksia kuten taustaväri ja koko.

Swing-kirjastosta

Luvussa 12.4 on johdanto käyttöliittymiin Swing-kirjastolla. Tuo valinnainen luku on sijoitettu kurssin ja tämän kurssimateriaalin loppupäähän. Jos aihe kovasti kutkuttaa, voit lukea sen aiemminkin. Riittävät esitiedot siihen sinulla on nyt periytymisestä opittuasi.

Option-hierarkia (ja suljetut yliluokat)

../_images/inheritance_option.png

Pieni hierarkia liittyy tuttuun Option-luokkaankin.

Jo luvussa 4.3 näit, että Option-tyyppisiä olioita on kahdenlaisia. Jokainen Option-tyyppinen olio on joko Some-olio jonkinlaisella sisällöllä tai None. Tässäkin on kyse periytymisestä: Option-tyyppi on abstrakti luokka, josta periytyvät konkreettinen luokka Some ja yksittäisolio None.

Luvussa 7.4 mainittiin, että piirreluokan voi sulkea sealed-sanalla, jolloin sille ei voi määritellä muita välittömiä alakäsitteitä kuin ne, jotka on kirjattu samaan tiedostoon. Yliluokankin voi sulkea, ja Option on juuri tällainen suljettu yliluokka: kuten luvussa 4.3 todettiin, Option voi olla joko Some tai None (jotka on määritelty samassa tiedostossa) mutta ei koskaan mikään muu. Se onkin Optionin nimenomainen tarkoitus. Emme voi itse määritellä Optionille muita aliluokkia, ja hyvä niin.

Kaikkien luokkien äiti: Any

Tutkitaan Scala-olioita REPLissä:

val sekalaisia = Vector(123, "laama", true, Vector(123, 456), Square(10), IArray(1, 2, 3))sekalaisia: Vector[Any] = Vector(123, laama, true, Vector(123, 456), o1.shapes.Square@114c3c7, Array(1, 2, 3))

Esimerkissä luodaan vektori, jossa on keskenään aivan erilaisia olioita: Int, String, Boolean, Vector[Int], Square ja IArray[Int]-tyyppinen alkiokokoelma.

Vektorin alkioiden tyypiksi päätellään Any. Kyseessä on "vektorillinen mitä tahansa olioita". Ilmeisesti siis myös kokonaisluvut, vektorit, neliöt jne. ovat Any-tyyppisiä?

Piirreluokkia lukuun ottamatta kaikki Scala-luokat ja yksittäisoliot — myös itse kirjoitetut — periytyvät automaattisesti Any-nimisestä luokasta, vaikka tätä ei erikseen koodiin normaalisti kirjatakaan. Aivan kaikki Scala-ohjelmissa käytetyt oliot ovat siis Any-tyyppisiä muiden tyyppiensä lisäksi.

Tätä kaikkien luokkien kantaluokkaa voi käyttää myös vaikkapa muuttujan tyyppinä, kuten seuraavassa REPL-esimerkissä:

var jokuOlio: Any = "kumkvatti"jokuOlio: Any = kumkvatti
jokuOlio.isInstanceOf[Any]res4: Boolean = true
jokuOlio.isInstanceOf[String]res5: Boolean = true
jokuOlio.isInstanceOf[Square]res6: Boolean = false
jokuOlio = Square(10)jokuOlio: Any = o1.shapes.Square@ecfb83
jokuOlio.isInstanceOf[Any]res7: Boolean = true
jokuOlio.isInstanceOf[String]res8: Boolean = false
jokuOlio.area-- Error: ... value area is not a member of Any

Kuten tyypin nimikin antaa ymmärtää, tällaiseen muuttujaan voi sijoittaa arvoksi viittauksen millaiseen tahansa olioon, vaikkapa merkkijonoon tai neliöön, kuten tässä.

Muuttujan jokuOlio staattinen tyyppi on siis Any, ja kutsu jokuOlio.isInstanceOf on luvallinen koska (ja vain koska) kyseinen metodi isInstanceOf on määritelty luokassa Any ja on näin käytettävissä mille tahansa Scala-oliolle.

Sen sijaan kutsu jokuOlio.area epäonnistuu, vaikka muuttujaan sattuukin olemaan tallennettuna Square-tyyppinen olio, jolla area-metodi on. Muuttujan staattinen tyyppi rajoittaa sitä, millaiset metodikutsut ovat sallittuja.

Useimmiten Any-tyyppinen muuttuja ei ole sovelias valinta, koska se rajoittaa muuttujan arvon käyttämistä liiaksi. Kun staattisena tyyppinä on Any, voi arvolla tehdä vain sellaisia asioita, jotka on määritelty Any-luokassa. Näitä kaikille Scala-oliolle yhteisiä metodeita ovat vain isInstanceOf, toString, ==, != sekä kourallinen muita. Muuttujille kannattaa yleensä valita jokin spesifisempi tyyppi, kuten olet kurssilla tähänkin mennessä tehnyt. (Vähän lisää aiheesta jäljempänä.)

Melkein kaikkien luokkien äiti: AnyRef eli Object

Scalan piällystyyppi Any jakautuu kahteen "päähaaraan". Sillä on välittömät aliluokat AnyVal ja AnyRef:

../_images/inheritance_any.png

Jako AnyRefiin ja AnyValiin liittyy Scala-kielen toteutukseen eikä ole erityisen keskeinen aloittelevan Scala-ohjelmoijan tai yleisemmin ohjelmoinnin perusteiden kannalta. Näistä tyypeistä on silti hyvä tietää sen verran, että hahmotat, miksi niiden nimet esiintyvät joskus Scaladoc-dokumenteissa, REPL-tulosteissa ja virheilmoituksissa.

AnyVal on yliluokka eräille sellaisille valmiille luokille, jotka edustavat tietynlaisia suhteellisen yksinkertaisia perustietotyyppejä ja joiden käyttö on tietyillä tavoin tehokkaampaa kuin muiden tietotyyppien. AnyValista periytyvät tutut tietotyypit Int, Double, Boolean, Char, Unit, ja muutama muu. AnyVal-luokalle on suhteellisen harvoin järkevää itse tehdä aliluokkia, tämän kurssin puitteissa ei koskaan.

AnyRef puolestaan on yliluokka kaikille muille (ei-piirre-)luokille ja yksittäisolioille. Esimerkiksi luokat String ja Vector sekä yllä itse laadittu luokka Item periytyvät AnyRefistä.

Tilannetta mutkistaa hieman se, että silloin, kun käytetään Scalaa Java-virtuaalikoneen "päällä" (kuten usein tehdään; luku 5.4), niin AnyRefistä käytetään toteutusteknisistä syistä myös nimeä Object.

AnyVal sekä AnyRef eli Object näkyvät myös REPLissä:

val sekalaisia2 = Vector(123, true)sekalaisia2: Vector[AnyVal] = Vector(123, true)
val sekalaisia3 = Vector("laama", Vector(123, 456), Square(10))sekalaisia3: Vector[Object] = Vector(laama, Vector(123, 456), o1.shapes.Square@667113)

Int- ja Boolean-tyypit periytyvät molemmat AnyValista.

String, Vector ja Square periytyvät AnyRefistä eli Objectista.

Vielä yksi: Matchable

Mainittujen "ylätyyppien" lisäksi voit törmätä vielä yhteen: Matchable on piirreluokka, joka kattaa yläkäsitteenä kaikki sellaiset Scala- tyypit, joita on luvallista käyttää hahmontunnistuksessa — siis match-käskyssä. Sekä AnyRef että AnyVal piirteisiin on liitetty Matchable-piirre, joten Matchable on käsitteenä lähes yhtä laaja kuin Any. (match-käskyä voi soveltaa melkein kaikkiin Scala-arvoihin, mutta on joitakin aivan poikkeuksellisia tyyppejä, joilla ei Matchable-piirrettä ole. Lisätietoja on saatavilla verkosta, mutta ne eivät ole kurssin kannalta merkityksellisiä.)

Ilmentymien räätälöintiä yläkäsitteistä

Luvussa 2.4 opimme luomaan olion, jolle on ilmentymäkohtaisesti räätälöity metodi luokan määrittelemien metodien lisäksi:

object terasmies extends Henkilo("Clark"):
  def lenna = "WOOSH!"
end terasmies// defined object terasmies

Olemme sittemmin käyttäneet samaa tekniikkaa View-luokan kanssa määritellessämme yksittäisiä View-olioita, joille olemme räätälöineet omia metodeita.

Tässä vaiheessa kurssia voimme todeta, että itse asiassa tämä ilmentymäkohtainen räätälöinti on esimerkki periytymisestä. Esimerkiksi yllä REPLissä annettu käsky periyttää Henkilo-yliluokasta yksittäisen olion. Vastaavasti olemme toteuttaneet View-yläkäsitteen abstraktiksi jättämän makePic-metodin erilaisilla tavoilla.

Yksittäistä olioita määritellessä voi yliluokkia ja piirreluokkia yhdistellä joustavasti muillakin tavoin. Voit halutessasi lukea siitä alta lisää.

Alakäsitteen määritteleminen "lennosta"

Scala mahdollistaa olioiden luomisen niin, että olioon liitetään piirreluokka "lennosta", luomiskäskyn yhteydessä.

Määritellään pohjustuksena pari piirreluokkaa ja yksi tavallinen luokka. Nämä kolme ovat toisistaan täysin erilliset:

class Elain(val laji: String):
  override def toString = "eläin, tarkemmin sanoen " + this.laji
trait Sylkeva:
  def sylje = "pthyi"
trait Kyttyrallinen:
  def kyttyroidenMaara: Int// defined class Elain
// defined trait Sylkeva
// defined trait Kyttyrallinen

Kokeillaan uutta tapaa tehdä olio:

val lemmikki = new Elain("laama") with Sylkevalemmikki: Elain & Sylkeva = eläin, tarkemmin sanoen laama

Määrittelemme "lennosta" uuden, tarkemmin nimeämättömän alatyypin, joka on Elain-luokan aliluokka ja johon on liitetty Sylkeva-piirre. Samalla luodaan tuosta luokasta saman tien yksi ilmentymä.

Sana new on pakollinen tällaisissa käskyissä, joissa ei ainoastaan luoda uutta ilmentymää vaan samalla määritellään sille uusi tyyppi. Huomaa myös with.

Tällä Elain & Sylkeva -yhdistelmätyyppiä olevalla oliolla voi siis tehdä asioita, joita on määritelty joko Elain-luokassa tai Sylkeva-piirreluokassa:

lemmikki.lajires9: String = laama
lemmikki.syljeres10: String = pthyi

Toki saman lopputuloksen saa myös määrittelemällä erikseen nimetyn luokan seuraavaan tapaan ja luomalla sitten tuosta luokasta ilmentymän.

class Laama extends Elain("laama"), Sylkeva// defined class Laama

Metodien lisääminen "lennosta" määriteltyyn tyyppiin

Jatketaan esimerkkiä. Luodaan olio, joka on sellaista tyyppiä,

  • joka on Elain-luokan aliluokka,

  • jolla on piirteet Kyttyrallinen ja Sylkeva, ja

  • joka toteuttaa Kyttyrallinen-piirreluokan abstraktin kyttyroidenMaara-metodin tietyllä tavalla.

val seSeOn = new Elain("dromedaari") with Kyttyrallinen with Sylkeva:
  def kyttyroidenMaara = 1seSeOn: Elain & Kyttyrallinen & Sylkeva = eläin, tarkemmin sanoen dromedaari

Katsotaan lopuksi, miten samalla tekniikalla voi luoda olion, jolla on Sylkeva-piirre ja omanlaisensa toString-metodi:

val tunnettuKalamies = new Sylkeva:
  val nimi = "Eemeli"
  override def toString = this.nimitunnettuKalamies: Sylkeva = Eemeli

Tässä kirjoitettiin new-sanan perään piirreluokan eikä tavallisen luokan nimi. Käsky ei kuitenkaan luo ilmentymää suoraan piirreluokasta (mitä ei voikaan tehdä). Se määrittelee uuden nimettömän alatyypin, jolla on tuo piirre ja mainitut lisäominaisuudet, ja luo ilmentymän tästä uudesta tyypistä.

Hakusanoja: scala trait mixin, scala anonymous subclass.

Lisälukemisto

Staattisen tyypin valitsemisesta

Staattiset tyypit yleensäkin, ja parametrimuuttujien staattiset tyypit eritoten, on useimmiten hyvä kirjata koodiin mahdollisimman "laajoiksi" eli käyttää yliluokkaa tai piirreluokkaa muuttujien tyyppinä. Näin metodeista ja luokista tulee yleiskäyttöisempiä ja helpommin muokattavia.

Vertaa:

def doSomething(circle: Circle) =
  // ...

vs.

def doSomething(shape: Shape) =
  // ...

Ensimmäinen määrittely on perusteltu, jos metodin todella on järkevää toimia ainoastaan ympyräolioille (esimerkiksi siksi, että se tarvitsee toimiakseen parametriksi saamansa ympyräolion sädettä, jollaista ei ole muilla kuvioilla). Muutoin jälkimmäinen määrittely on yleensä parempi, koska metodi toimii nyt erilaisille kuvioille, myös mahdollisille Shape-piirreluokan vielä luomattomille alatyypeille.

Aina ei voi yleistää; muutenhan kaiken tyypiksi tulisi Any. Mutta yleistä kun voit.

Julkisen ja yksityisen väliltä: protected

Aliluokan olioillakin on yliluokan private-muuttujat osana tietojaan. Yliluokalta perityt metodit käyttävät noita muuttujia. Mutta aliluokan koodista ei voi suoraan viitata yliluokan private-osiin. Vastaavasti myöskään piirreluokan liittävät luokat eivät pääse suoraan käsiksi piirreluokan yksityisiin osiin.

Scalassa, kuten joissakin muissakin kielissä, on käytettävissä myös näkyvyysmääre protected. Se sallii juuri tuon äsken mainitun, jota private ei salli: protected-muuttujaa tai metodia voi käyttää luokan itsensä lisäksi alakäsitteiden koodissa (mutta ei vapaasti mistä tahansa muualta, kuten julkisia osia). Lisätietoja löydät netistä.

Moniperintä

Ylempänä mainittiin, että luokalla voi kirjata vain yhden yliluokan. Miksi ei saisi periä kuin yhdestä välittömästä yliluokasta? Selvitä netitse, mitä on moniperintä (multiple inheritance). Voit myös selvittää, mikä on moniperintään liittyvä "tuomion timantti" ("deadly diamond of death") ja miksi jotkut pitävät sitä ongelmana ja toiset eivät.

Hieman provokatiivista mutta mielenkiintoista lisäluettavaa (lähinnä Javaa ja sen rajapintaluokkia ennestään tunteville):'Interface' Considered Harmful.

Jos liittää useita piirreluokkia, mitä metoditoteutusta käytetään?

Tässä pieni esimerkki:

trait X:
  def metodi: String
trait A extends X:
  override def metodi = "a:n metodi"
trait B extends X:
  override def metodi = "b:n metodi"// defined trait X
// defined trait A
// defined trait B
class AB extends A, B
class BA extends B, A// defined class AB
// defined class BA
AB().metodires11: String = b:n metodi
BA().metodires12: String = a:n metodi

Lisätietoja hakusanoilla scala linearization tai Kirjoja ja linkkejä -sivun lähteistä.

Liskovin periaate

../_images/liskov-fi.png

"Neliö suorakaiteen aliluokkana" on klassinen esimerkki, kun tarkastellaan Liskovin periaatetta (Liskov substitution principle), josta on tässä annettu epätäsmällinen mukaelma. Tämän periaatteen noudattamista pidetään eräänä olio-ohjelman laadun kriteerinä.

Luokille, jotka on laadittu Liskovin periaatetta noudattaen, pätee:

Jos S on T:n aliluokka, niin on mahdollista ja mielekästä kohdistaa luokasta S luotuun ilmentymään mikä tahansa sellainen toimenpide, jonka voi kohdistaa luokasta T luotuun ilmentymään.

Toisin sanoen: T:n ilmentymän paikalle sopii yhtä hyvin myös S:n ilmentymä.

Palataan esimerkkiin. Aiemmin tässä luvussa määriteltiin tällaiset luokat:

class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape:
  def area = this.sideLength * this.anotherSideLength
class Square(size: Double) extends Rectangle(size, size)

Mieti, onko tämä ohjelmakoodi Liskovin periaatteen mukainen vai ei. Entä jos muutettaisiin Rectangle-luokasta val- sanat vareiksi? Muista: yllä määriteltiin luokan Square laatimisen tavoitteeksi, että kaikkien Square-olioiden tulee edustaa sellaisia suorakaiteita, joiden sivut ovat keskenään samanmittaiset.

Lue lisää aiheesta vaikkapa Wikipediasta:

Käsitteiden välisistä riippuvuuksista

Piirreluokkaluvussa 7.3 oli jo esillä ajatus siitä, että ylä- ja alakäsitteen välillä on epäsymmetrinen suhde: alakäsite määritellään yläkäsitteen avulla muttei toisin päin. Sama pätee periytymiseenkin:

  • Aliluokan koodiin kirjataan, mistä yliluokasta aliluokka periytyy ja voidaan siis luottaa siihen, että aliluokan olioillakin on tietyt perityt metodit käytettävissä. Aliluokan koodissa voidaan kutsua this.yliluokastaPerittyMetodi. Aliluokasta voi myös super-sanalla viitata nimenomaisesti yliluokan osiin.

  • Periytymistä ei merkitä yliluokkaan, ja yliluokan koodi onkin sikäli aliluokista riippumaton. Yliluokan koodissa ei voi this-oliolle kutsua mahdollisten aliluokkien erityismetodeita vaan vain metodeita, jotka koskevat koko yläkäsitettä. super-sanalla ei ole vastinetta, joka viittaisi periytymishierarkiassa alaspäin.

Jos yliluokat olisivat riippuvaisia aliluokistaan, niin muutokset aliluokkiin usein aiheuttaisivat muutoksia yliluokan toimintaan. Tämä olisi monesti harmillista. Esimerkiksi: Ei ole harvinaista periä jokin ohjelmakirjastossa määritelty luokka (vaikkapa View). Kirjaston laatija ei voi etukäteen tai muutenkaan tuntea kaikkia aliluokkia eikä reagoida niihin tuleviin muutoksiin.

Toisaalta on toivottavaa, että yliluokkaan voi lisätä uutta toiminnallisuutta metodeina, ja nuo metodit tulevat käyttöön kaikkiin aliluokkiin. Käytännössä tämä usein onnistuukin, mutta asiaan liittyy myös eräs periytymisen heikkous:

Yliluokkien hauraudesta

Ilmaisu hauraan yliluokan ongelma (fragile base class problem) viittaa tilanteisiin, joissa yliluokkaa ei voi muuttaa tuntematta aliluokkien yksityiskohtia.

Yksi esimerkki ongelmasta on metodin lisääminen yliluokkaan niin, että se "rikkoo" aliluokan, jonne oli satuttu tekemään samanniminen metodi. Scalan tapauksessa tällöin syntyy käännösaikainen virhe, joka valittaa override-määrittelyn puuttumisesta aliluokassa. Sellaisissa toisissa kielissä, jotka eivät vaadi override-merkintää, seurauksena voi olla yllättävä virheellinen toiminta ohjelma-ajon aikana.

Aiheeseen palataan olio-ohjelman suunnittelun yhteydessä kevään puolella. Voit hakea tietoa esimerkiksi Wikipedian artikkelista; ks. myös Composition over inheritance -periaate.

Korvaamisen ja alakäsitteiden estäminen: final

Jos kirjoitat defin eteen sanan final, ei tuota metodia voi korvata aliluokassa. Metodi periytyy alatyyppien olioillekin sellaisenaan. Korvausyritys tuottaa käännösaikaisen virheilmoituksen.

Sama sana final luokkamäärittelyn alussa ennen class-sanaa estää kokonaan aliluokkien määrittelemisen tuolle luokalle. (Vrt. luokan sulkeminen sanalla sealed, joka estää muut välittömät aliluokat kuin samassa tiedostossa määritellyt.)

Sopivasti käytettynä final-määre voi parantaa ohjelman ymmärrettävyyttä tai ehkäistä luokkien epätarkoituksenmukaista käyttöä. Joissain tapauksissa myös ohjelman tehokkuus paranee, kun kääntäjän ei tarvitse huomioida korvaavien toteutusten mahdollisuutta.

Scalan peruskirjastossa on monia final-luokkia.

Yhteenvetoa

  • Periytyminen on olio-ohjelmointitekniikka, jossa yläkäsitettä kuvaavan luokan eli yliluokan ominaisuudet periytyvät alakäsitteitä kuvaaville aliluokille.

  • Luokka voi olla abstrakti, jolloin siinä voi olla abstrakteja metodeita ja muuttujia kuten piirreluokassakin. Abstraktista luokasta ei voi suoraan luoda ilmentymiä vaan vain aliluokkiensa kautta.

  • Piirreluokilla ja abstrakteilla yliluokilla on paljon yhteistä mutta myös eroja. Erot riippuvat ohjelmointikielestä (ja kaikissa kielissä ei ole kumpaakin käsitettä erikseen).

    • O1-kurssilla laadimme näistä lähinnä piirreluokkia. Kurssin tarpeisiin abstrakteista luokista ei tarvitse tietää juuri muuta kuin että ne muistuttavat piirreluokkia.

  • Kaikki Scala-luokat kuuluvat hierarkiaan, jonka kantaluokkana on valmis luokka Any.

  • Lukuun liittyviä termejä sanastosivulla: periytyminen eli perintä, aliluokka, yliluokka, tyyppihierarkia, Any; abstrakti luokka; staattinen tyyppi, dynaaminen tyyppi; avoin luokka, suljettu luokka; final.

Palaute

Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.

Tekijät

Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!

Materiaalin luvut tehtävineen ja viikkokoosteineen on laatinut Juha Sorva.

Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.

Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, 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 ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.

Tapa, jolla käytämme O1Libraryn työkaluja (kuten Pic) yksinkertaiseen graafiseen ohjelmointiin, on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.

Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki. Pääkehittäjänä on nyt Markku Riekkinen, jonka lisäksi A+:aa ovat kehittäneet kymmenet Aallon opiskelijat ja muut.

A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen 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

a drop of ink
Palautusta lähetetään...