SISÄLLYS:
Antero Pulli 14.06.1996
Olio-ohjelmoinnin keskeisiä tavoitteita ovat ohjelmakoodin uudelleenkäytettävyyden saavuttaminen sekä tietojen ja niitä käsittelevien toiminnallisten osien kokoaminen samoihin, vahvan sisäisen koheesion omaaviin yksiköihin. Edellämainitut tavoitteet saavutetaan suunnittelemalla luokkahierarkia huolellisesti. Kun lisäksi tiedonkätkentää käytetään taiten, yksiköt (luokat) luovat vahvan tietoabstraktion ja ovat esitystapariippumattomia.
Olio-ohjelmoinnin tärkeimpiä käsitteitä ovat luokka, periytyminen ja monikäyttöisyys. Luokka on klassisen oliomallin peruskäsite, joka vastaa lähinnä perinteisen ohjelmointikielen tietotyyppiä. Luokka määrittelee olion, luokasta luotavan ilmentymän, ominaisuudet. Luokan käyttäjää kutsutaan asiakkaaksi. Periytyminen mahdollistaa yliluokan ominaisuuksien käyttämisen aliluokassa. Luokat ja niiden väliset periytymissuhteet muodostavat luokkahierarkian.
Luokka on olio-ohjelmoinnissa olion ominaisuuksien, menetelmien ja attribuuttien, määritelmä, johon on koteloitu sekä olion tiedot että toiminta (Budd 1991). Luokan ilmentymän rakenteen määräävät tietokentät, attribuutit, voivat olla olioita, jolloin puhutaan koosteoliosta. Koosteolioon sisältyvää toista oliota kutsutaan luokan osaolioksi (Budd 1991). Luokan perimät ja luokassa määritellyt ominaisuudet määräävät luokan ilmentymän toiminnan. Luokan menetelmää kutsuttaessa menetelmä saa, yleensä implisiittisenä parametrina, tiedon menetelmän kohdeoliosta eli oliosta jonka menetelmää kutsutaan. Tätä menetelmän tietoa kohdeoliosta kutsutaan itseviitteeksi (Budd 1991).
Tietoabstraktion saavuttamiseksi luokan määrittelyssä käytetään tiedon kätkentää. Ominaisuudet, joita luokan asiakkaan ei ole tarkoitus käyttää, määritellään joko yksityisiksi tai suojatuiksi. Yksityiset ja suojatut ominaisuudet ovat luokan asiakkaiden saavuttamattomissa (Budd 1991). Ominaisuudet, joihin luokan asiakkaan on tarpeellista päästä käsiksi, määritellään julkisiksi.
Luokka voi olla toteutukseltaan joko abstrakti tai konkreetti. Abstrakti luokka sisältää ainakin yhden viivästetyn menetelmän (Ellis & Stroustrup 1990). Menetelmä on viivästetty, jos menetelmästä on luokan kuvauksessa ainoastaan esittely, mutta toteutus on jätetty avoimeksi. Abstraktista luokasta ei voida luoda ilmentymiä, vaan luokkaa on tarkoitus käyttää periytymisessä yliluokkana. Konkreeteissa luokissa kaikki menetelmät ovat toteutettuja ja konkreeteista luokista voidaan luoda ilmentymiä.
Luokka on geneerinen, jos luokan kuvauksessa esiintyy argumenttina ainakin yksi parametroitu tyyppi (Budd 1991). Korvaamaalla geneerisen luokan parametroidut tyypit todellisilla tyypeillä voidaan geneerisestä luokasta luoda todellisia luokkia. Geneerisyys voi olla syntaktista (Cleaveland 1986), jolloin geneerisyys saavutetaan makroja vastaavalla tekniikalla eli kääntäjä generoi jokaiselle todelliselle luokalle oman koodinsa. Toinen geneerisyyden muoto on semanttinen geneerisyys (Cleaveland 1986), jossa geneerisen luokan menetelmät voidaan kääntää jo geneerisinä.
Periytyminen on kahden luokan välinen binäärinen relaatio. Periytyminen mahdollistaa yliluokkassa määriteltyjen ominaisuuksien käytettämisen aliluokassa. Periytyminen on transitiivista eli aliluokka perii ominaisuudet myös kaikilta yliluokkansa esivanhemmilta. Luokan esivanhempia ovat luokan yliluokka ja yliluokan esivanhemmat. Vastaavasti luokan jälkeläisluokkia ovat luokan aliluokka sekä aliluokan jälkeläiset.
Johdettaessa uusi luokka jostakin toisesta luokasta, johdetun luokan ilmentymän katsotaan sisältävän yliluokan ilmentymän aliolionaan. Aliluokassa voidaan yliluokasta perittyjä ominaisuuksia kumota eli määritellä uudestaan. Luokat ja niiden väliset periytymissuhteet muodostavat luokkahierarkian, joka voidaan kuvata suunnattuna syklittömänä verkkona.
Periytyminen jaetaan yksittäisperiytymiseen ja moniperiytymiseen (Budd 1991). Yksittäisperiytymisessä aliluokalla on ainoastaan yksi yliluokka, jolta se perii ominaisuudet. Moniperiytymisessä aliluokalla voi olla useita yliluokkia. Moniperiytyminen voi olla riippumatonta, jolloin luokan yliluokilla ei ole yhteisiä esivanhempia, tai haarautuvaa, jolloin luokka perii jonkin esivanhemman ominaisuudet useampaa kuin yhtä reittiä. Kun jälkeläisluokka perii esivanhempiluokkansa useampaa eri reittiä, puhutaan toistuvasta periytymisestä. Jos aliluokka perii saman luokan yliluokkana useampaan kertaan, on toistuva periytyminen välitöntä.
Periytymistä käytetään olio-ohjelmoinnissa hyvin eri tavoin ja hyvin erilaisiin tarkoituksiin. Erikoistaminen on periytymisen lajeista yleisin ja tärkein. Erikoistavassa periytymisessä aliluokan ilmentymä on yliluokan ilmentymän erikoistapaus (Halbert & O'Brien 1987). Erikoistavan periytymisen erityistapauksena pidetään määrittelyä (Budd 1991). Määrittelevässä periytymisessä kaikki käytettävät menetelmät, luokan liitymä, kuvataan jo yliluokassa, mutta niiden toteutus jätetään avoimeksi eli yliluokat ovat abstrakteja. Myös toteutus on yleisesti käytetty periytymisen laji. Toteutuksessa yliluokan ilmentymät ovat yksinään käyttökelvottomia, mutta niitä käytetään aliluokan llmentymän osana (Halbert & O'Brien 1987). Moniperiytyminen periytymisen lajina on yhdistävää periytymistä (Halbert & O'Brien 1987).
Periytymisen lajeista yleistäminen ja muuntelu ovat jossain määrin periytymisen väärinkäyttöä (Halbert & O'Brien 1987), mutta tarpeellisia joissakin tapauksissa. Yleistävä periytyminen on erikoistavan periytymisen vastakohta, jota joudutaan käyttämään, kun yliluokkaa ei jostain syystä voida muuttaa (yliluokka on esimerkiksi kirjastoluokka). Kahden luokan välinen periytyminen on muuntelevaa periytymistä, jos yli- ja aliluokan välillä ei ole erikoistamissuhdetta, mutta niillä on paljon yhteisiä ominaisuuksia. Tällaisessa tilanteessa tulisi luokkien yhteiset ominaisuudet siirtää näiden yhteiseen yliluokkaan. Muuntelevan periytymisen käyttäminen on soveliasta, jos edellä kuvatuille luokille ei voida luoda yhteistä yliluokkaa (alkuperäinen yliluokka on esimerkiksi kirjastoluokka). Periytyminen on laajentavaa (Budd 1991) jos aliluokka lisää yliluokkaan uusia ominaisuuksia ja rajoittavaa (Budd 1991) jos aliluokka ei tarjoa omille aliluokilleen tai asiakkailleen kaikkia kaikkia yliluokkansa näkyviä ominaisuuksia.
Budd (1991) määrittelee monikäyttöisyyden muuttujan ominaisuudeksi, jonka ansiosta muuttujan arvoksi voidaan asettaa eri tyyppejä olevia arvoja. Menetelmä on monikäyttöinen, jos sillä on ainakin yksi monikäyttöinen argumentti (implisiittinen itseviite mukaanlukien). Myös menetelmän, jonka nimeä on kuormitettu tekemällä siitä useita eri toteutuksia, katsotaan olevan monikäyttöinen.
Luokkahierarkiaan kuuluvien luokkien ilmentymät ovat monikäyttöisiä, koska luokasta periytymisellä johdetun aliluokan ilmentymää voidaan käsitellä myös yliluokan ilmentymänä aliluokan ilmentymän sisältäessä kaikki yliluokan ilmentymän ominaisuudet. Edellämainitun ansiosta voidaan sekä yli- että aliluokan ilmentymiin soveltaa yliluokan menetelmiä. Koska yliluokan menetelmät voidaan kumota aliluokassa, voidaan luokkien ilmentymiin soveltaa myös samannimisiä, mutta toteutukseltaan erilaisia menetelmiä.
Monikäyttöisyys voi olla joko yleistä tai erityistä (Cardelli & Wegner 1985). Yleisessä monikäyttöisyydessä toteutukseltaan samaa menetelmää voidaan soveltaa eri luokkien ilmentymiin. Erityisessä monikäyttöisyydessä suoritetaan samannimisen menetelmän eri toteutuksia eri luokkien ilmentymille. Erityisestä monikäyttöisyydestä esimerkkejä ovat menetelmien ja operaattoreiden kuormitus sekä yksinkertaisimmillaan automaattinen tyypinmuunnos. Yleinen monikäyttöisyys jaetaan sisältyvään ja parametriseen monikäyttöisyyteen. Sisältyvää monikäyttöisyyttä esiintyy periytymisessä yliluokan menetelmien periytyessä aliluokalle. Yliluokan menetelmät ovat käytössä myös aliluokissa ja menetelmiä kutsuttaessa suoritetaan aina sama yliluokassa toteutettu koodi. Parametrisessa monikäyttöisyydessä menetelmät voidaan toteuttaa tietämättä argumenttien todellista tyyppiä. Parametrista monikäyttöisyyttä esiintyy esimerkiksi geneeristen luokkien toteutuksissa.
C++-kielen olio-ominaisuuksista
C++-kielessä (Ellis & Stroustrup 1990) voidaan luokan ominaisuudet määritellä yksityisiksi, suojatuiksi tai julkisiksi. Luokassa yksityisiksi määrätyt ominaisuudet eivät näy luokan ulkopuolelle, eivät edes luokan aliluokille. Suojatut ominaisuudet näkyvät luokan aliluokille, mutta eivät luokan asiakkaille, ja julkiset ominaisuudet näkyvät sekä aliluokille että asiakkaille. Luokalle voidaan määritellä tuttavaluokkia, jolloin luokan kaikki ominaisuudet ovat tuttavaluokan käytössä. Myös perittyjen luokkien (ja niiden ominaisuuksien) näkyvyys voidaan määrätä käyttäen samoja määreitä kuin luokassa määriteltävien ominaisuuksien yhteydessä.
template<class T> class A { T x; public: void A(T& i) {x=i;}; }; ... A<int> B(10); A<double> C(0.1); Esimerkki 1.1
Parametroitujen tyyppien käyttö mahdollistaa syntaktisesti geneeristen luokkien toteuttamisen C++-kielellä. Esimerkissä 1.1 on kuvattu geneerinen luokka A<T>. Luokassa on yksi geneerinen argumentti, parametroitu tyyppi T. Luokka voidaan kääntää tällaisenaan, tietämättä T:n todellista tyyppiä. Geneerisistä luokista luodaan todellisia luokkia, kuten esimerkin 1.1 A<int> ja A<double>, muuttujien tyypinmäärittelyden yhteydessä. Esimerkin 1.1 oliot B ja C eivät ole saman luokan ilmentymiä, sillä C++:ssa luokkien samuus perustuu nimiyhtäläisyyteen. Jos esimerkissä 1.1 myös muuttujan C tyyppi olisi A<int>, olisivat B ja C saman luokan ilmentymiä.
Abstrakteja luokkia C++-kielessä määritellään jättämällä vähintään yksi luokan menetelmä toteuttamatta ja ilmoittamalla luokan kuvauksessa kyseinen menetelmä viivästetyksi (Ellis & Stroustrup 1990). Tällaisesta luokasta ei voida luoda ilmentymiä.
class A { public: void M(); }; class B : public A { public: void M(); }; ... A* C; B* D=new B; ... C=D; C->M(); D->M(); Esimerkki 1.2
Esimerkissä 1.2 luokka B on luokan A aliluokka. Kun ohjelmakoodissa annetaan muuttujalle C tyypiksi osoitin luokkaan A, on muuttujan C staattinen tyyppi A. Kun muuttujaan C sijoitetaan osoitin D, jonka staattinen tyyppi on B, on muuttujan C dynaaminen tyyppi B. Ellei menetelmän esittelyssä ole toisin määritelty, menetelmän kutsu aiheuttaa olion staattisessa luokassa toteutetun menetelmän suorituksen. Esimerkissä 1.2 menetelmän M kutsu C->M(); suorittaa luokassa A toteutetun menetelmän M ja kutsu D->M(); luokassa B toteutetun menetelmän M, vaikka muuttujat C ja D osoittavatkin samaan olioon. Menetelmä voidaan kuitenkin määritellä virtuaaliseksi, jolloin menetelmän kutsussa käytetään myöhäistä sidontaa eli kutsu aiheuttaa olion dynaamisessa luokassa toteutetun menetelmän suorituksen (Ellis & Stroustrup 1990).
Jos esimerkissä 1.2 olisi menetelmä M määrätty luokassa A virtuaaliseksi, suorittaisivat sekä kutsu C->M(); että D->M(); luokassa B toteutetun menetelmän M. Puhtaissa oliokielissä, kuten Smalltalk, kaikki menetelmät ovat virtuaalisia. Virtuaalimenetelmä ja sen uudelleenmäärittelyt jälkeläisluokissa muodostavat luokkahierarkiassa menetelmäperheen. C++:ssa virtuaalimenetelmän signatuuri ei saa muuttua menetelmää jälkeläisluokissa kumottaessa.
C++-kielessä on mahdollistettu yksittäisperiytymisen lisäksi myös moniperiytyminen. Moniperiytymisen käyttäminen voi aiheuttaa nimikonflikteja aliluokassa, aliluokka perii signatuuriltaan samanlaiset menetelmät tai samannimiset attribuutit kahdesta eri luokkahierarkiasta. C++-kielessä ongelma on ratkaistu uudelleennimeämisellä eli yliluokkien ominaisuudet voidaan nimetä uudelleen aliluokassa ja ominaisuuksiin voidaan viitata aliluokasta niiden uusilla nimillä. Jos luokka perii eri luokkahierarkioista menetelmät, joilla on sama nimi, mutta menetelmien signatuurit ovat toisistaan poikkeavat, ei menetelmiä tarvitse nimetä uudelleen, vaan kyse on menetelmien kuormittamisesta.
Moniperiytyminen tuo mukanaan toistuvan periytymisen mahdollisuuden: vaikka C++ ei salli välitöntä toistuvaa periytymistä, luokkaa A luokan B yliluokkana useampaan kertaan, luokka B voi haarautuvassa periytymisessä periä luokan A useampaa eri reittiä. Toistuvassa periytymisessä luokan A olio monistuu (Ellis & Stroustrup 1990) ja luokalla B on aliolioinaan yhtä monta luokan A oliota kuin on periytymisreittejä luokasta A luokkaan B. Kieli mahdollistaa myös aliolion jakamisen. Jakaminen saadaan aikaan määrittelemällä periytymissuhde virtuaaliseksi.
class A {}; class B : public virtual A {}; class C : public B {}; class D : public B {}; class E : public C, public D {}; Esimerkki 1.3
Esimerkissä 1.3 on määritelty seuraava luokkahierarkia: luokka A on luokan B yliluokka, luokan A periytyminen luokkaan B on määrätty virtuaaliseksi, luokka B on luokkien C ja D yliluokka sekä luokka E on luokkien C ja D aliluokka. Esimerkin tapauksessa luokan E ilmentymällä on aliolioinaan luokkien C ja D ilmentymät, joilla molemmilla on aliolionaan luokan B ilmentymät. Luokalla E siis on aliolioina kaksi luokan B ilmentymää. Luokalla E on kuitenkin alioliona ainoastaan yksi luokan A ilmentymä, jonka luokan B ilmentymät jakavat yhteisenä aliolioinaan, koska luokan A periytyminen luokkaan B on määrätty virtuaaliseksi. Esimerkin 1.3 kaltaisessa luokkahierarkiassa periytyminen voi aiheuttaa aiemmin käsitellystä poikkeavia nimikonflikteja.
class A { public: virtual void M(); }; class B : public virtual A {}; class C : public B { public: void M(); }; class D : public B {}; class E : public C, public D {}; Esimerkki 1.4
Esimerkissä 1.4 on rakenteeltaan esimerkin 1.3 kanssa yhtenevän luokkahierarkian luokassa A määritelty virtuaalimenetelmä M ja menetelmä on kumottu luokassa C. Ongelmana on, periikö luokka E virtuaalimenetelmän M luokasta C (toteutettu luokassa C) vai luokasta D (toteutettu luokassa A). C++:ssa on ongelma ratkaistu siten, että edellämainitun kaltaisissa tilanteessa on voimassa dominanssisääntö (Sakkinen 1992). Säännön mukaan luokka E perii virtuaalimenetelmän M toteutuksen esivanhempiluokastaan L, jos menetelmän M kaikki muut kumoamiset tapahtuvat luokan L esivanhemmissa. Muissa tapauksissa menetelmä M täytyy kumota myös luokassa E. Esimerkissä 1.4 on luokan A menetelmä M kumottu luokassa C, mutta ei luokassa D, joten luokka E perii menetelmän M toteutuksen luokasta C. Jos menetelmä M olisi kumottu luokan C lisäksi myös luokassa D, täytyisi menetelmä M kumota myös luokassa E.
class C { public: U A; }; class D : public C { public: T A; void M(){A=...; C::A=...;}; }; void F(C c){ c.A=... } ... F(D d); Esimerkki 1.5
C++ mahdollistaa menetelmien kumoamisen lisäksi myös attribuuttien kumoamisen. Esimerkissä 1.5 luokassa C määritelty attribuutti A kumotaan luokan C aliluokassa D. Luokan D C-osassa säilyy kuitenkin attribuutti A, siihen ei vain päästä käsiksi luokasta D muuten kuin viittaamalla yliluokkaosan C kautta, kuten luokan D menetelmässä M on tehty. Esimerkin 1.5 funktio F saa muodollisena argumenttina luokan C ilmentymän c. Kun funktiolle annetaan kutsuttaessa todellisena argumenttina luokan D ilmentymä d, funktion rungossa tapahtuva viittaus attribuuttiin A kohdistuu ilmentymän d C-osan attribuuttiin A. Jos funktion F muodollisen argumentin tyyppi olisi luokka D, viittaukset attribuuttiin A kohdistuisivat luokassa D määriteltyyn attribuuttiin A.
Attribuuttiviittauksen kohdistuminen aina ilmentymän staattisessa luokassa määriteltyyn attribuuttiin johtuu siitä, että attribuuttiviittauksissa käytetään aikaista sidontaa, eli attribuutti, johon viitataan, on tiedossa jo ohjelman käännösaikana. Attribuuttiviittauksiin voitaisiin käyttää myöhäistä sidontaa, jos olioattribuutit voitaisiin määritellä virtuaalisiksi. Olioattribuutin määrittely virtuaaliseksi vaatisi kuitenkin, että kumottaessa attribuuttia aliluokassa aliluokan attribuutin luokan tulisi olla yliluokan attribuutin jälkeläisluokka, eli kumoaminen olisi kovarianttia. C++:ssa attribuutti voidaan kumota aliluokassa minkätyyppiseksi tahansa, joten attribuuteilla ei ole välttämättä minkäänlaista yhteistä liittymää.
Budd T. A. 1991: An Introduction to Object-Oriented Programming. Addison-Wesley, Reading, Massachusetts. ISBN 0-201-54709-0.
Cardelli L. & Wegner P. 1985: On Understanding Types, Data Abstraction, and Polymorphism. Computing Surveys, 17(4) 471-522. ISSN 0360-0300.
Cleaveland J. G. 1986: An Introduction to Data Types. Addison-Wesley, Reading, Massachusetts. ISBN 0-201-11940-4.
Ellis M. A. & Stroustrup B. 1990: The Annotated C++ Reference Manual. Addison-Wesley, Reading, Massachusetts. ISBN 0-201-51459-1.
Halbert D. C. & O'Brien P. D. 1987: Using Types and Inheritance in Object-Oriented Programming. IEEE Software, 4(5) 71-79. ISSN 0740-7459.
Sakkinen M. 1992: Inheritance and Other Main Principles of C++ and Other Object-oriented Languages. University of Jyväskylä, Jyväskylä. ISBN 951-680-817-4.
Antero Pulli
antero.pulli@tieto.com
Dokumentti on osa kandidaatintutkielmaa, jonka tekijä laati opiskellessaan Joensuun yliopiston tietojenkäsittelytieteen laitoksessa. Dokumentin on tekijän luvalla ja Juha Hakkaraisen avustuksella palauttanut Webiin Jukka Korpela.