TIES542 kevät 2009 Oliokielten erityispiirteitä Antti-Juhani Kaijanaho 2. maaliskuuta 2009 Oliosuuntautuneisuus (engl. object-orientation) ja olio-ohjelmointi (engl. objectoriented programming) ovat laajassa käytössä olevia sanoja, joilla on tietty merkitys, mutta tuo merkitys on yksityiskohdiltaan varsin sumuinen jokaisella ekspertillä on oma käsityksensä siitä, mitä olio-ohjelmointi on. Booch (1994) määritteli olio-ohjelmoinnin seuraavasti (s. 38, suom. AJK): Olioohjelmointi on toteutusmenetelmä, jossa ohjelmat rakentuvat kokoelmina yhteistyötä tekeviä olioita, jotka jokainen edustaa jonkin luokan ilmentymää, ja jonka luokat ovat kaikki perintäsuhteiden kautta yhdistyneen luokkahierarkian jäseniä. Taivalsaari (1993) luonnehtii olio-ohjelmointia seuraavasti (s. 30, suom. AJK): mikä tahansa systeemi, joka tukee oliokäsitettä ja tarjoaa keinot olioiden inkrementaaliselle muokkaukselle, voidaan tulkita oliosuuntautuneeksi.. Armstrong (2006) luettelee metatutkimuksessaan oliopohjaisen kehityksen keskeiset termit ja määrittelee ne seuraavasti (suom. AJK): olio (engl. object): yksittäinen, yksilöity kappale (engl. item), joko todellinen tai abstrakti, joka sisältää tietoa siitä itsestään sekä kuvauksia kyseisen tiedon käsittelystä. luokka (engl. class): yhden tai useamman samankaltaisen olion yhteisen rakenteen ja toiminnan kuvaus. kapselointi (engl. encapsulation): luokkien ja olioiden suunnittelutekniikka, joka rajoittaa pääsyä tietoon ja käyttäytymiseen määrittelemällä rajoitetun joukon viestejä, jotka kyseisen luokan olio voi vastaanottaa. metodi (engl. method): tapa asettaa, hakea taikka käsitellä olion tietoa. viestinvälitys (engl. message passing): prosessi jolla olio lähettää tietoa toiselle oliolle tai pyytää tätä herättämään (engl. invoke) tietyn metodin. polymorfismi (engl. polymorphism): eri luokkien kyky vastata samaan viestiin ja toteuttaa se sopivalla kullekin tavalla. 1
abstraktio (engl. abstraction): luokkien luominen todellisuuden osa-alueiden yksinkertaistamiseksi käyttäen ongelmaan sinänsä kuuluvia eroavaisuuksia. Tässä monisteessa tarkastellaan oliokielten erityispiirteitä. Monisteessa rakennetut mallikielet ovat muunnelmia Abadin ja Cardellin (1996) sigmalaskennosta. 1 Olio Booch (1994) ja Taivalsaari (1993) määrittelevät olion (engl. object) samalla tavalla: oliolla on tila (engl. state), käyttäytyminen (engl. behavior) ja identiteetti (engl. identity). Tässä monisteessa hyväksyn tämän määritelmän, kuitenkin todeten, että olio on dynaaminen (ajonaikainen), ei staattinen (käännösaikainen) käsite. Tarkastellaan seuraavaksi sen seurauksia. Olion identiteetillä tarkoitetaan sitä olion ominaisuutta (tai ominaisuuksia yhdistelmää), joka erottaa olion toisesta oliosta. Identiteetti ilmenee identiteettivertailuoperaattorin (esimerkiksi Javan ==) toiminnassa. Identiteetti myös mahdollistaa olion löytämisen jos tiedät olion identiteetin, pääset olioon käsiksi ja näin muistuttaa eräiden mytologioiden todellinen nimi -ajatusta (jos tiedät jonkun todellisen nimen, sinulla on valta hänen ylitseen). Käytännössä olion identiteetti on yleensä oliolle varatun muistialueen alkuosoite. Joskus se ei riitä, esimerkiksi jos kielessä on tuki hajautetuille olioille, jolloin olion identiteettiin tulee lisätä myös jonkinlainen tieto siitä, missä koneessa olio sijaitsee. Hajauttamattomassakin järjestelmässä olion osoite ei välttämättä ole hyvä identiteetti: ajatellaanpa nyt vaikkapa kopioivaa muistinsiivousta taikka pysyviä olioita, jotka tallennetaan levylle ja ladataan takaisin muistiin, yleensä eri osoitteisiin. Useimmissa oliokielissä olion identiteetti on implisiittinen, mutta joissakin kielissä se voi olla myös eksplisiit Olion tilalla tarkoitetaan sen sisältämää dataa. Yleensä olion tila koostuu yhdestä tai useammasta nimetystä muuttujasta eli attribuutista, jotka jokainen tallettaa jonkin arvon. Tässä mielessä olio muistuttaa tietuetta. Se, että olion tila ja identiteetti mainitaan määritelmässä erikseen, tarkoittaa, että olion tila ei ole riippuvainen sen identiteetistä eli olion tila voi muuttua ohjelman suorituksen edetessä. Toisin sanoen kielessä tulee olla tuki sijoituslauseelle ja sivuvaikutuksille. Olion käyttäytyminen tarkoittaa sitä, että olio voi vastaanottaa toiselta oliolta viestin ja reagoida siihen muuttamalla tilaansa taikka lähettämällä muita viestejä. Se, että käyttäytyminen liitetään nimenomaan olioon, dynaamiseen käsitteeseen, 2
tarkoittaa, että tuo käyttäytyminen voi periaatteessa olla jokaisella oliolla erilainen, toisin sanoen käyttäytymisen määrittelevä ohjelmakoodi on jollakin tapaa sisällytettävä olioon itseensä. Oliolle lähetettävä viesti sisältää tavallisesti viestin nimen sekä jonon viestiin liittyvää dataa. Tavallisesti viestin lähettäjä odottaa oliolta vastausta, joka sisältää (tavallisesti) yhden arvon paluutietona tätä sanotaan synkroniseksi viestiksi. Synkroninen viestinvälitys voidaan nähdä aliohjelmakutsun varianttina, jolloin viestin lähetys vastaa aliohjelmakutsua ja vastausviesti vastaa aliohjelmasta palaamista. Jos viestiin ei odoteta vastausta (asynkroninen viesti), voidaan tämä nähdä uuden säikeen luomisena, ja tällöin vastaanottava olio käsittelee viestin tuossa uudessa säikeessä. Koodia, joka käsittelee viestin, on se synkroninen tai asynkroninen, sanotaan olion metodiksi. Metodi voi periaatteessa olla kullekin oliolle erikseen koodattu, mutta silloin uusia olioita ei voida dynaamisesti luoda. Parempi tapa on parametrisoida metodi yli olion, jolloin se saa itseviitteen (engl. self-reference), eli sen olion identiteetin, johon se kuuluu, (implisiittisenä tai eksplisiittisenä) parametrinaan. Yksinkertainen (tyypitön) oliokieli voidaan määritellä seuraavasti (tosin käytännön toimintaa varten kieli pitänee laajentaa primitiiviarvoilla ja -operaatioilla kuten aritmetiikalla): l Labels x, y, z Variables r References t, u Terms t, u ::= x { l 1 = t 1,..., l k = t k, l k+1 = m 1,..., l n = m n k } olion luonti t.l attribuutin valinta t.l(t 1,..., t n ) viestin lähetys t.l u attribuutin korvaus t; u peräkkäistys m MethodConstruction m ::= ςxλ(x 1,..., x n ) t v Values v ::= r 3
Kielessä muuttujia sitoo ainoastaan metodi ςxλ(x 1,..., x n ) t, joka sitoo t:ssä muuttujat x ja x 1,..., x n. Muut muuttujat ovat vapaita. Laskentasäännöissä välitilat koostuvat termeistä ja keoista (osittaisfunktio viitteiltä termeille), lopputilat koostuvat arvoista ja keoista O on { l 1 = v 1,..., l k = v k, l k+1 = m 1,..., l n = m n k } (O, σ) (r, σ { (r, O) }) (r, {..., l i = v i,... }) σ (r.l i, σ) (v i, σ) (r, {..., l i = ςxλ(x 1,..., x n ) t,... }) σ (r.l i (v 1,..., v n ), σ) (t[x := r][x 1 := v 1 ]... [x n := v n ], σ) r dom σ (E-new) (E-sel) (E-call) (r, { l 1 = v 1,..., l i = v i,..., l k = v k, l k+1 = m 1,..., l n = m n k }) σ (r.l i v, σ) (r, { l 1 = v 1,..., l i = v,..., l k = v k, l k+1 = m 1,..., l n = m n k }) (E-ass) (v; t, σ) (t, σ) (E-sel) Säännöt, jotka sallivat lausekkeiden sisällä laskemisen, jätetään tässä kirjoittamatta näkyviin. Pääsääntönä on, että kaikkialla saa laskea, poikkeuksena seuraavat: metodin sisällä ja peräkkäistyksen oikeanpuolimmaisessa operandissa ei saa laskea. Metodikonstruktiossa ςxλ(x 1,..., x n ) t muuttuja x on itseviittaus ja viittaa siihen olioon, jonka metodista on kyse; loput muuttujat ovat viestidataa (eli normaaleja parametreja). Tässä kielessä (laajennettuna tavanomaisella aritmetiikalla) laskuriolio voitaisiin määritellä seuraavasti: { n = 0, } clear = ςsλ() s.n 0, inc = ςsλ() s.n s.n + 1 2 Inkrementaalinen muokkaus Tällainen kieli ei tue vielä inkrementaalista muokkausta, jota Taivalsaari (1993) pitää oliokielen olennaisena ominaisuutena. Yksinkertainen tapa sallia inkrementaalinen muokkaus on lisätä kieleen metodinkorvausoperaatio t.l i σxλ(x 1,..., x n ) t, joka toimii kuten sijoitus paitsi että se muuttaa metodia, ei attribuuttia. Seuraavassa tarkastellaan muita vaihtoehtoja. 4
2.1 Prototyyppiperintä Prototyyppiperinnässä yksittäisen olion käyttäytymistä ei voi muokata (kuten metodinkorvausmenetelmässä voi), mutta oliosta voidaan tehdä muokattuja kopioita. Tässä ideana on lisätä muokkaava kloonausoperaatio t, u ::= t { l 1 = t 1,..., l k = t k, l k+1 = m 1,..., l n = m n k } jonka laskentasääntö on sen verran sotkuinen, että en sitä kaavana kirjoita: 1. Laske ensin t ja jokainen t 1,..., t k arvoksi asti. 2. Hae keosta t:n edustama olio. 3. Luo kekoon uusi olio, jonka attribuutteja ovat l i = t i ja jonka metodeja ovat l i = m i k ja jossa on lisäksi jokainen t:n metodi ja attribuutti, joka ei ole samanniminen kuin mikään l i. Idea on siis, että olio on kopio toisesta oliosta (sen prototyyppi) poiketen siitä kuitenkin annetulla tavalla. Konkreettisessa toteutuksessa attribuutit kopioidaan aina mutta metodeille voidaan tehdä kolme erilaista ratkaisua: Metoditkin (tai osoittimet niihin) voidaan kopioida uuteen olioon. Uuteen olioon lisätään delegaatioviite vanhaan olioon, ja uuteen olioon tallennetaan vain muokatut metodit. Mikäli uudelle oliolle lähetetään viesti, jota se ei itse osaa käsitellä, delegoi se kyseisen viestin vanhalle oliolle (kuitenkin niin, että metodin itseviittaus viittaa uuteen, ei vanhaan olioon). 2.2 Luokkaperintä Tavallisempi tapa hoitaa inkrementaalinen muokkaus on lisätä kieleen tuki luokille (engl. class), joilla on seuraavat ominaisuudet: 1. Jokainen olio kuuluu yhteen (ja vain yhteen) luokkaan. 2. Jokaisella luokan oliolla on sama rakenne ja samat metodit. Luokat, toisin kuin oliot, ovat yleensä staattisia otuksia, mutta ne voivat olla dynaamisiakin (onpa joissakin kielissä niinkin, että luokatkin ovat olioita 1 ). 1. Mikä on luokan luokka? 5
Seuraavassa tyypittömässä kielessä luokat ovat dynaamisia otuksia mutta eivät olioita: l Labels x, y, z Variables t, u Terms t, u ::= x { l 1 = t 1,..., l k = t k, l k+1 = m 1,..., l n = m n k } luokan luonti new t olion luonti t.l attribuutin valinta t.l(t 1,..., t n ) viestin lähetys t.l u attribuutin korvaus t; u peräkkäistys m MethodConstruction m ::= ςxλ(x 1,..., x n ) t Konstruktio { l 1 = t 1,..., l k = t k, l k+1 = m 1,..., l n = m n k }, joka edellisessä kielessä loi olion, luo nyt luokan. Ajonaikana luokka voidaan esittää tietueena, joka sisältää sen metodit mutta ei attribuutteja, sekä viittauksen ohjelmapätkään (konstruktori), joka luo luokasta olion. Laajemmissa kielissä mutta ei tässä konstruktoreita voi olla useita ja ne sattavat olla parametrisoituja. Olio luodaan luokasta operaattorilla new, jolle annetaan luokka operandiksi: se kutsuu luokan konstruktoria. Luokkaa ei saa käyttää attribuutin valinnan, attribuutin korvauksen taikka viestin lähetyksen ensimmäisenä operandina, sillä luokka ei ole olio. Inkrementaalinen muokkaus sallitaan tällaisessa kielessä lisäämällä luokan perintä: t, u ::= t { l 1 = t 1,..., l k = t k, l k+1 = m 1,..., l n = m n k } Tässä t on luokka; se luo luokan, jossa on kaikki t:n attribuutit ja metodit sekä kaikki eksplisiittisesti nimetyt attribuutit (l i = t i ) ja metodit (l i = m i k ), kuitenkin siten, että eksplisiittisesti nimetty attribuutti tai metodi korvaa uudessa luokassa alkuperäisen luokan attribuutin tai metodin. Huomaa, että tyypitetyssä kielessä luokan ei tarvitse olla sama asia kuin tyyppi. Ero on samankaltainen kuin Javan luokkien ja rajapintojen välillä: tyyppi kertoo, mitä viestejä on lupa lähettää, kun taas luokka kertoo, minkälaisia oliot ovat. 6
Tarkastellaan kieliä, joissa on seuraavat ominaisuudet: 1. Kieli on staattisesti tyypitetty. 2. Kieli soveltaa nimiyhtäläisyyttä. 3. Oliotyyppiin täsmälleen yksi luokka. 4. Luokalla on vain yksi yliluokka. Tällaisia kieliä ovat (moniperintä sivuuttaen) esimerkiksi C++ ja Java. Tällaisessa kielessä luokka voidaan esittää ajonaikana metodiosoittimien taulukkona, jossa on ensiksi kaikki yliluokan metodit ja sitten esittelyjärjestyksessä luokan omat metodit. Kääntäjä kykenee tällöin staattisesti antamaan jokaiselle metodille yksikäsitteisen järjestysnumeron (joka on riippumaton siitä, mihin aliluokkaan olio kuuluu), jolloin metodikutsu voidaan kääntää nopeaksi sekvenssiksi: 1. Hae kohdeolion luokka. 2. Etsi tuosta luokasta n:s metodi, missä n on kutsuttavan metodin (käännösaikana selvitetty) järjestysnumero. 3. Kutsu kyseistä metodia. 3 Tiedonpiilotus Tiedonpiilotus (engl. information hiding), joidenkin kirjoittajien mukaan kapselointi (engl. encapsulation), tarkoittaa kielen ominaisuutta, jossa osa olion vastaanottamista viesteistä (taikka sen attribuutit) piilotetaan niin, että olion identiteetin tietäjä ei pysty niitä käyttämään. Luokkakielissä tämä voidaan tehdä siten, että attribuutille tai metodille (joita yhteisesti sanotaan ominaisuuksiksi) annetaan annotaatio public (kaikki saavat käyttää), protected (luokka itse ja sen aliluokat saavat käyttää) ja private (vain luokka itse saa käyttää). Joissakin kielissä voidaan antaa tarkempiakin määrittelyjä: C++:ssa voidaan sallia protected- ja private-ominaisuuksien käyttäminen nimetyille luokille käyttämällä friend-määrittelyä. Eiffelissä voidaan erikseen luetella luokat, jotka pääsevät käsittelemään tiettyjä ominaisuuksia. Tiedonpiilotus voidaan toteuttaa myös erillisenä ominaisuutena modulijärjestelmän (joka on mahdollinen myös muissa kuin oliokielissä) avulla. Moduli on (yleensä) staattinen tai (harvoin) dynaaminen konstruktio, joka sisältää joukon määrittelyitä, joista osa ovat modulin sisäisiä ja osa ovat julkisia. Moduli voidaan ottaa käyttöön (engl. import) toisessa modulissa erityisellä käyttöönottokonstruktiolla, minkä jälkeen kyseisen modulin julkiset määrittelyt ovat käyttöön ottaneen modulin sisällä käytettävissä. 7
Tiedonpiilotus mahdollistaa erillisen kääntämisen: jokainen moduli (tai luokka) voidaan kääntää erillään toisesta siten, että vain julkisen rajapinnan muutos vaatii käyttäjämodulien uudelleen kääntämisen. Tällä on suurissa järjetelmissä olennainen merkitys käännösajan kannalta. Viitteet Martín Abadi and Luca Cardelli. A Theory of Objects. Springer, 1996. Deborah J. Armstrong. The quarks of object-oriented development. Communications of the ACM, 49(2):123 128, February 2006. Grady Booch. Object-oriented analysis and design with applications. Addison Wesley, second edition, 1994. Antero Taivalsaari. A Critical View of Inheritance and Reusability in Objectoriented Programming. PhD thesis, University of Jyväskylä, 1993. 8