Ohjelmoinnin peruskurssien laaja oppimäärä Luento 10: Perintä, oliot vs. moduulit Riku Saikkonen (merkityt ei-laajan kurssin kalvot: Otto Seppälä) 26. 1. 2011
Sisältö 1 Ohjelmointikielten moduulijärjestelmistä 2 Oliot ja tavanomainen perintä 3 Metodien korvaaminen periessä 4 Olioiden toteutuksesta
Moduulit isommat ohjelmat jaetaan yleensä moduuleihin (module) moduulissa on sisäisiä muuttujia ja funktioita eli oma nimiavaruus moduulissa on (pitäisi olla?) selkeä ulkoinen rajapinta: esim. funktiot, joita moduuli tarjoaa (export) muulle koodille tavoite: ulkoisen rajapinnan dokumentaatio riittää moduulin käyttämiseen, eli sen sisuksista ei tarvitse välittää (moduuli tarjoaa yksittäistä funktiota suuremman kokonaisuuden abstraktion) mistä moduulien rajat keksitään? usein moduuli tekee tietyn toiminnon tai joukon tiettyyn asiaan liittyviä toimintoja tai käsittelee tiettyä (moduulin sisäistä) tietorakennetta esimerkkejä: jonotietorakenne, tietyn tiedostomuodon käsittely, debug-loggaus, lautapelin tekoäly yleiskäyttöisiä moduuleja ryhmitellään usein kirjastoiksi (library) terminologia vaihtelee hieman eri ohjelmointikielissä
Eri ohjelmointikielten tuki moduuleille useimmat ohjelmointikielet tarjoavat moduuleihin liittyen: tavan määritellä moduulin rajat näkyvyysasetukset: mitkä proseduureista tms. näkyvät muualle tavan kertoa moduulien riippuvuuksista (esim. import) usein moduuli = kooditiedosto esim. C-kielessä on tiedoston sisäisiä muuttujia ja funktioita (päätasolla oleva static) yleensä ohjelmia ei käännetä kokonaan kerralla vaan yksi tällainen moduuli/tiedosto kerrallaan joissakin kielissä koodissa on erillinen osa, joka kertoo vain moduulin ulkoisen rajapinnan (ei toteutusta) esim. C:n header-tiedostot ja ML:n signaturet moduulia käyttävää koodia voi kääntää tämän avulla (ilman moduulin toteutuksen lähdekoodia)
Lisäominaisuuksia moduulituessa useimmat kielet tukevat moduulien ryhmittelyä tai sisäkkäisiä moduuleita C:ssä kaksi tasoa: kooditiedosto ja kirjasto Javassa ja Pythonissa hierarkkiset nimet (esim. java.util.stack) muutama muu lisäominaisuus eri kielistä: osittainen import (Java, Python) osien uudelleennimeäminen importissa (Python) parametroidut moduulit (ML, Ocaml) moduulien tekeminen toisten moduulien pohjalta (Java, ML, Ocaml) moduulit kielessä käsiteltävänä arvona (PLT Scheme/Racket)
Moduulit oliokielissä Javassa moduulijako on hieman toisenlainen luokka moduuli (mutta olioissa on muutakin) paketti moduuli tai kirjasto (kirjasto on yksi tai useampi paketti) usein oliokielissä näkyvyydet (public, private yms.) määritellään metodien yhteydessä toinen vaihtoehto: kaikki metodit ovat periaatteessa public, mutta erillään niistä kerrotaan, mitkä luokat ja mitkä niiden metodeista kuuluvat moduulin julkiseen rajapintaan näin esim. Common Lispin oliojärjestelmässä etuja: moduulin rajapinta selvemmin yhdessä paikassa; sopii paremmin kieleen, jossa päätasolla on muutakin kuin luokkia haitta: metodiin liittyvä metatieto on koodissa hajallaan (näkyvyys eri paikassa kuin tyyppi yms.)
Miten moduulit toteutetaan? koodin jako moduuleihin onnistuisi ilman ohjelmointikielen tukea noudattamalla tarkkaa nimeämiskäytäntöä esim. Schemessä ja Emacs Lispissä näin usein tehdäänkin tämä ei toki isoissa ohjelmissa ole kovin kätevää... näkyvyydet voidaan tarkistaa käännösaikana siis (tavallisessa) moduulijärjestelmässä moduulien ei tarvitse näkyä ajon aikana teoriassa moduulijärjestelmän voisi toteuttaa esikäsittelemällä koodin ennen kääntäjälle antamista (tarkistetaan näkyvyydet ja nimetään kaikki uudestaan) käytännössä kuitenkin näkyy mm. debuggauksen ja osittaisen kääntämisen vuoksi moduulien toteuttamiseksi pitää myös määritellä mm. mistä muita moduuleja haetaan
Sisältö 1 Ohjelmointikielten moduulijärjestelmistä 2 Oliot ja tavanomainen perintä 3 Metodien korvaaminen periessä 4 Olioiden toteutuksesta
Millainen abstraktio olio on? olio-ohjelmoinnissa ratkaistava ongelma jaetaan osiin luokkien ja yksittäisten olioiden avulla puhtaimmillaan olio on (yleensä) itsenäinen kokonaisuus jolla on omaa tilaa (kentät) joka reagoi viesteihin (= metodikutsuihin) muuttamalla tilaansa tai lähettämällä muille viestejä olio-ohjelmoinnin tavoite (?): olioita käyttävä ohjelma on (vain) joukko itsenäisiä olioita, jotka hoitavat kukin omaa tehtäväänsä ja tarvittaessa kommunikoivat keskenään viesteillä ohjelman suorituksen aikana olioiden joukko muuttaa tilaansa alkutilasta (syötteen määräämään) tavoitetilaan useimmissa kielissä oliot tehdään luokkien avulla
(ei-laajan kurssin kalvo) Perintä (inheritance) Perintä on menetelmä, jonka avulla jostakin olemassaolevasta luokasta voidaan johtaa uusi luokka, joka saa automaattisesti käyttöönsä perimänsä luokan ominaisuuksia. Perittävää luokkaa kutsutaan yliluokaksi (superclass) Uutta, yliluokan ominaisuudet perivää luokkaa kutsutaan aliluokaksi (subclass) Periminen ilmaistaan luokan nimen jälkeen tulevalla sanalla extends, jota seuraa perittävän luokan (yliluokan) nimi. public public class class User User { { public public void void setpassword setpassword ( ( String String pass pass ) ) { {...toteutus...toteutus public public class class Admin Admin extends extends User User { {...... toteutus toteutus 02:10
(ei-laajan kurssin kalvo) Perintä (inheritance) Esimerkissä Admin-luokka perii luokan User Nyt Admin luokalla on perinnän kautta toteutus metodille setpassword. Luokka perii yliluokalta myös sen toteuttamat metodit, rajapinnat, yliluokat sekä kentät. Käytännössä Admin-oliot ovat myös yliluokkiensa (User ja Object) tyyppisiä (IsA-suhde) public public class class User User { { public public void void setpassword setpassword ( ( String String pass pass ) ) { {...toteutus...toteutus public public class class Admin Admin extends extends User User { {...... toteutus toteutus 02:10
(ei-laajan kurssin kalvo) Esimerkki Kirjaston tietokanta sisältää nykyisin varsin monenlaisia lainattavia asioita Kirjoja Kasetteja CD:itä Videoita DVD-levyjä Nuotteja, mikrofilmejä, jne... Näillä on paljon yhteisiä ominaisuuksia Luokkia mallinnettaessa yhteiset ominaisuudet tulisi mahdollisuuksien mukaan kerätä yliluokkaan. 02:10
(ei-laajan kurssin kalvo) Esimerkki: ennen perintää Luokissa on yhteisiä tai melkein yhteisiä toteutusosia Koodia toistetaan turhaan Ohjelmaa muutettaessa samat korjaukset tehdään kaikkiin luokkiin public class CD { public String getisbn(){... public String[] getartists(){... public class Book { public String getisbn(){ public Time getplaytime() {...... public String[] getauthors(){... public class Magazine { public String getisbn(){ public int getpages()... {... public String[] geteditors(){... public String getissn(){... 02:10
(ei-laajan kurssin kalvo) Esimerkki: perinnän jälkeen Yhteiset piirteet luokassa LibraryItem Kaikilla aliluokilla metodit getisbn ja getauthors public class LibraryItem { public String getisbn(){... public String[] getauthors(){... public class CD extends LibraryItem { public Time getplaytime() {... public class Book extends LibraryItem { public int getpages() { public class Magazine extends LibraryItem { public String getissn(){... 02:10
(ei-laajan kurssin kalvo) Perintähierarkia Yhdellä luokalla voi olla monta aliluokkaa ja millä tahansa aliluokalla taas omia aliluokkia jne. Perintähierarkia on termi, jolla viitataan siihen kuinka luokat perinnän kautta muodostavat puumaisen rakenteen jonka huipulla on Javassa luokka Object. Object Person JComponent String jne... Admin User JScrollPane 02:10
(ei-laajan kurssin kalvo) Perintä ja näkyvyys Protected-määre Private-määre rajaa näkyvyyden vain ja ainoastaan saman luokan olioihin. Public-määre tarjoaa täyden näkyvyyden koko maailmalle Protected sijoittuu puoliväliin Näkyy samaan luokkaan Näkyy kaikkiin kyseisen luokan aliluokkiin Pakettinäkyvyys (package-private) Neljäs näkyvyysmääre, joka ilmaistaan jättämällä näkyvyysmääre kirjoittamatta näkyy samaan pakettiin, mutta ei välitä perintäsuhteista 02:10
Perinnän ongelmia 1/2 edellisestä kirjastoesimerkistä: entä jos aluksi olisi tiedossa vain LibraryItemin tiedot, ja vasta myöhemmin selviäisi että olio onkin CD? entä jos sama LibraryItem sisältäisi kirjan ja CD:n? em. ongelmat syntyvät siitä, että oliolla on vain yksi luokka eikä sitä voi luomisen jälkeen muuttaa vrt. että LibraryItemin tyyppi ja lisätiedot olisivat kenttiä toisenlainen ongelma: mitä toimintoja ja koodia esim. jokin CD-olio kaiken kaikkiaan sisältää? tai: mihin koodiin on mahdollista mennä, kun CD-luokan metodi kutsuu toista saman olion metodia? periaatteessa pitää käydä koko perintähierarkia läpi dokumentointi ja ohjelmointityökalut (esim. Eclipse) auttavat mm. tämänkaltaisista syistä perintää käytetään usein paljon vähemmän kuin voisi kuvitella luokkahierarkiaa aluksi suunnitellessa syntyy helposti paljon syvempi hierarkia kuin mitä lopulta käytetään
Perinnän ongelmia 2/2 entä jos yliluokan toimintaa joutuu muuttamaan olennaisesti? kaikki aliluokat pitää käydä läpi ja tarkistaa siis yliluokkien toimintoja suunnitellessa pitää olla varovainen, jos aliluokkia on paljon mitä metodeja saa muuttaa luokkaa periessä ja miten? (siis mikä on perimisen kannalta luokan sisäistä tietoa) monimutkaista määritellä, joten jätetään usein tekemättä usein käytännössä tulee paljon tyhmiä metodeja, jotka vain välittävät viestejä eteenpäin toiselle oliolle tai rajapinnalle toinen joukko tyhmiä metodeja ovat useimmat get- ja set-metodit sekä jotkin konstruktorit monissa oliokielissä (mm. Common Lisp, Scala) onkin tapa tehdä näitä automaattisesti osalle näistä ongelmista on vastine myös ei-olio-ohjelmoinnissa
(ei-laajan kurssin kalvo) Perinnän etuja Koodia tarvitsee kirjoittaa vähemmän Uudelleenkäyttö (Reuse) Koodin ylläpito selkiytyy Muutokset yliluokkaan päivittyvät myös aliluokkiin Mallinnusnäkökulma Polymorfismin edut Olioita voidaan tarvittaessa käsitellä luokasta riippumatta yliluokan tasolla. 02:10
Sisältö 1 Ohjelmointikielten moduulijärjestelmistä 2 Oliot ja tavanomainen perintä 3 Metodien korvaaminen periessä 4 Olioiden toteutuksesta
(ei-laajan kurssin kalvo) Perintä ja konstruktorit Konstruktorit eivät periydy Jos teit konstruktorin joka saa vaikkapa parametrin tyyppiä String, täytyy aliluokkaan tehdä samanlainen konstruktori mikäli sellaista tarvitsee. Yliluokkien konstruktorit suoritetaan aina Jokaisen aliluokan konstruktori kutsuu aina yliluokan konstruktoria ennen kuin suorittaa oman osuutensa Käytännössä tämä tapahtuu laittamalla konstruktorin alkuun kutsu super(parametrit); aliluokan konstruktorin alkuun. Toimii samalla tavoin kuin kutsu this(parametrit); Jos kutsua ei laita, kääntäjä toimii kuin konstruktorissa kutsuttaisiin aluski yliluokan parametritonta konstruktoria Parametriton konstruktori on implisiittisesti olemassa jos muita konstruktoreita ei ole määritelty Jos yliluokkaan on kirjoitettu jokin konstruktori, täytyy aliluokan 02:10 kostruktorin kutsua yhtä määritellyistä konstruktoreista
(ei-laajan kurssin kalvo) Korvaaminen (Overriding) Perinnässä aliluokan metodi voi halutessaan korvata yliluokan toteutuksen toteuttamalla metodin jolla on sama puumerkki. Metodia kutsuttaessa suoritetaan aina aliluokan versio Aliluokan versio metodista korvaa yliluokan version 02:10
Samannimiset metodit mikä on metodin puumerkki (signature)? riippuu ohjelmointikielestä staattisesti tyypitetyissä oliokielissä (mm. Java) usein metodin nimi ja tyyppi (argumenttien ja paluuarvon tyyppi) dynaamisesti tyypitetyissä usein vain metodin nimi Javassa voi olla monta samannimistä metodia erityyppiset ovat eri metodeita (overloaded) aliluokan metodi korvaa (override) vain samantyyppisen metodin samannimisistä kutsuttava metodi valitaan argumenttien staattisen (käännösaikaisen) tyypin perusteella Esimerkkikoodi (Javaa) class A { void foo(integer x) {... void foo(string x) {... public static void main(string[] args) { A a = new A(); a.foo(new Integer(1)); a.foo("5");
(ei-laajan kurssin kalvo) Korvaaminen (Overriding) Lisätään kirjastoesimerkkiin metodi getdetails() Metodi palauttaa merkkijonoesityksen kulloisestakin esineestä. Metodin voi korvata aliluokassa, jolloin eri esineet voivat tulostaa erilaisen kuvauksen tyypistään riippuen Ongelma Nyt jokaisen aliluokan täytyisi toteuttaa ISBN-kenttien tms. tulostukset toisistaan erillään. Turhaa toistoa Voidaanko yliluokan getdetails-metodia käyttää apuna? 02:10
(ei-laajan kurssin kalvo) Korvaaminen (Overriding) Aliluokasta (ja vain sieltä) voidaan eksplisiittisesti kutsua yliluokan metodia super.metodi( parametrit ); Toisin kuin konstruktoreissa, tämän kutsun saa tehdä missä tahansa metodissa ja missä tahansa kohdassa. Yleensä kutsu tehdään vastaavassa aliluokan metodissa public abstract class LibraryItem { public String getdetails() { return Title: \t + title + \n + Authors:... etc... public class Magazine extends LibraryItem { public String getdetails() { return super.getdetails() + \n + ISSN: \t + issn + \n +... etc... public abstract class AudioItem extends LibraryItem { public String getdetails() { return super.getdetails() + \n + playtime: \t + reclength + \n +... etc... 02:10
Miten pitäisi periä? olio-ohjelmoinnissa aliluokan olio on (is a) myös yliluokan jäsen luokkaa periessä pitäisi aina säilyttää yliluokan toiminnot siis mihin tahansa koodiin, jossa käytetään yliluokan oliota, voisi sijoittaa aliluokan olion, niin että koodi edelleen toimii järkevästi toisin sanottuna: jokaisen yliluokan metodin (myös niiden, joita ei periessä korvaa!) semantiikka eli merkitys pitää säilyttää tämä nk. substituutioperiaate (Liskov Substitution Principle, LSP) on eräs perinnän käyttämisen perusohjeista käytännössä tästä kuitenkin aina välilllä poiketaan varsinkin yksityiskohdissa ja samalla oletetaan, ettei yliluokkaa käytetä väärin... usein ei ole tarpeeksi tarkkaan mietitty, mitä metodin pitäisi tehdä
Substituutioperiaatteen seurauksia Substituutioperiaate formaalisti ((sub)type (ali)luokka) Let φ(x) be a property provable about objects x of type T. Then φ(y) should be true for objects y of type S where S is a subtype of T. B. Liskov, J. Wing: A behavioral notion of subtyping. ACM Trans. on Prog. Lang. and Syst., 16(6), 1994. metodien tyyppeihin liittyen substituutioperiaatteesta seuraa: metodin argumentin tyyppiä voi aliluokassa yleistää muttei tiukentaa (argumenttityyppi on kontravariantti) metodin paluutyyppiä voi aliluokassa tiukentaa muttei yleistää (paluutyyppi on kovariantti) esim. Java ei kuitenkaan salli argumenttityypin muuttamista metodien ja koko olion toimintaan liittyen siitä seuraa mm: esiehtoja ei saa tiukentaa aliluokassa jälkiehtoja ei saa heikentää aliluokassa aliluokan olio ei saa muuttua yliluokan metodeista näkyvällä tavalla, joka ei olisi mahdollinen yliluokassa
Miksi perintä on tärkeää? miksi olio-ohjelmoinnissa puhutaan niin paljon juuri perinnästä? vaikka olio-ohjelmoinnissa on paljon muutakin, perinnän tuki erottaa olio-ohjelmointikielet muista kielistä usein monimutkaisemmat olio-ohjelmoinnin rakenteet (esim. suunnittelumallit) tarvitsevat perintää ilmankin perintää voi tehdä paljon olio-ohjelmointia esim. SICPin olioesimerkit (mm. jono, rajoitteiden vyörytys) moduuli joka käsittelee tiettyä tietorakennetta olio joten olioihin pohjautuvaa suunnittelua voi tehdä myös muissa kuin oliokielissä oliosuunnittelun perusidea: mallinnetaan ratkaistava ongelma itsenäisinä olioina, jotka kommunikoivat viesteillä; toteutuksen ei tarvitse olla oikea olio näin vältetään perinnän aiheuttamia ongelmia (mutta menetetään osa abstraktiomahdollisuuksista) jotkut eivät kuitenkaan pidä tätä varsinaisena olio-ohjelmointina
Sisältö 1 Ohjelmointikielten moduulijärjestelmistä 2 Oliot ja tavanomainen perintä 3 Metodien korvaaminen periessä 4 Olioiden toteutuksesta
Mitä metodikutsu tekee? Esimerkkikoodi (Javaa) class A { int foo(int x) { return x + 1; class B extends A { int foo(int x) { return x + 2; class Main { int printfoo(a obj) { A a = obj; System.out.println(a.foo(5)); public static void main(string[] args) { B obj = new B(); printfoo(obj); kumpaa foo()-metodia tässä kutsutaan?
(ei-laajan kurssin kalvo) Polymorfismi Polymorfismi Aliluokan olioihin voi viitata yliluokan tyyppisen muuttujan kautta. Kun tämän muuttujan kautta kutsutaan metodeja, suoritetaan silti aina toteutus joka sijaitsee aliluokista alimmassa. (korvaaminen) Tämän avulla eri oliot voidaan saada toimimaan eri tavoin niiden luokasta riippuen kun käsitellään joukkoa olioita yliluokan metodeja kutsuen. For-silmukassa jokainen kappale tallennettiin vuorollaan muuttujaan item. GetDetails-metodin kutsu suorittaa muuttujan osoittaman olion todellisen luokan version metodista getdetails. ArrayList<LibraryItem> allitems = new ArrayList<LibraryItem>(); allitems.add( new CD(...)); allitems.add( new Book(...)); allitems.add( new Magazine(...)); for (LibraryItem item : allitems) { System.out.println(item.getDetails()); allitems.get(2).destroyproperly(); 02:10
Dynaaminen vs. staattinen metodikutsu kutsuttava metodi valitaan siis dynaamisesti eli ajon aikana (dynamic dispatch, dynamic method lookup) eli ajon aikana joudutaan tekemään hiukan ylimääräistä työtä proseduurikutsuun verrattuna (nykyään merkityksetöntä) pohjimmiltaan juuri tämä ominaisuus määrittelee, mikä on olio-ohjelmointikieli tarkemmin: kieli tukee olio-ohjelmointia, jos se tarjoaa mahdollisuuden tehdä dynaamista metodinvalintaa toinen vaihtoehto olisi valita metodi olion käännösaikaisen tyypin mukaan eli staattisesti monet moduulijärjestelmät toimivat periaatteessa näin (esim. ML) koska metodi on tiedossa käännösaikana, tämän voisi toteuttaa esikäsittelemällä koodia (automaattisesti tai käsin) mm. C++-kielessä on molemmat vaihtoehdot
Javan static-metodit Java-metodin voi määritellä staticiksi, jolloin sitä voi kutsua pelkällä luokan nimellä static-metodi ei voi käyttää thisiä (sillä luokan oliota ei välttämättä edes ole) oikeastaan static-metodit eivät ole olio-ohjelmointia vaan osa Javan moduulijärjestelmää sillä metodinvalinta ei ole dynaamista ne siis käyttäytyvät kuin (hierarkkisesti nimetyt) globaalit funktiot toinen tapa ajatella: static-metodi on luokan metodi, ja kutakin luokkaa on ajon aikana olemassa vain yksi samoin static-kentät
Mitä oliosta on tallessa muistissa? ajon aikana luokat ja metodit ovat tiedossa (käännösaikana luotuja vakioita) sen sijaan kenttien arvot ovat jokaiselle oliolle yksilöllisiä ajon aikana jokaisesta oliosta on tallessa: kenttien arvot viittaus luokkaa ja metodeita kuvaaviin vakioihin mikä sitten on metodi? proseduuri, joka saa ylimääräisenä implisiittisenä argumenttina olion, jonka kautta metodia kutsuttiin (tästä tulee this) Python-kielessä tämä argumentti näkyy koodissa eksplisiittisesti metodista on siis muistissa vain koodi vrt. proseduuri Scheme-tulkissa: koodi ja määrittely-ympäristö (sisäkkäisten metodien tukemiseen tarvitsisi myös määrittely-ympäristöä)