220 likes | 334 Views
Kevätlukukausi 2010 Jyväskylän yliopisto Tietojenkäsittelytieteiden laitos Markku Sakkinen. Komponenttipohjainen ohjelmistotekniikka (TJTSS56) Osa 6. Perinteisissä oliokeskeisissä ohjelmistokehyksisssä laajentaminen perustuu ensi sijassa periytymiseen
E N D
Kevätlukukausi 2010 Jyväskylän yliopisto Tietojenkäsittelytieteiden laitos Markku Sakkinen Komponenttipohjainen ohjelmistotekniikka (TJTSS56)Osa 6
Perinteisissä oliokeskeisissä ohjelmistokehyksisssä laajentaminen perustuu ensi sijassa periytymiseen Kehyksen luokille (yleensä abstrakteille) määritellään aliluokkia. Jos jokin kehyksen abstraktio ei sovi rakennettavaan sovellukseen tarpeeksi hyvin, aliluokat eivät toteuta korvattavuutta eli periytyminen ei ole aitoa erikoistamista. Tämä on mahdollista, koska toteutuksen perintä nykyisissä kielissä ei vaadikaan aliluokilta yliluokkien mukaista semantiikkaa. Perinteiset ohjelmistokehykset ovat vain kehitysaikaisia ”olioita”, jotka eivät ole toimivassa ohjelmistossa erillisinä näkyvissä. Huomattavia ongelmia periytymisen kyseenalaisesta käytöstä tulee vasta sitten, jos ohjelmisto haluttaisiin siirtää kehyksen uuteen versioon. Kehys ja siihen perustuva ohjelmisto eivät ole toisistaan riippumattomasti laajennettavia. Polymorfisuus (jatkoa)Riippumattoman laajennettavuuden ulottuvuuksiaSzyperski, kohta 6.9 Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6 3. 3. 2010 2
Riippumattomasti laajennettavissa komponenttijärjestelmissä tilanne on hyvin toisenlainen. Jokaisessa komponentissa täytyy varautua siihen, että sen käyttämiä tai palvelemia komponentteja vaihdetaan jopa ajon aikana. Sopimusten tarkka toteuttaminen on välttämätöntä. Tarvitaan selvät määritelmät siitä, mitä ja miten voidaan laajentaa. Jokaista tällaisen järjestelmän piirrettä, jota voidaan laajentaa erikseen, kutsutaan (riippumattoman) laajennettavuuden ulottuvuudeksi (dimensioksi). Olisi hyvä, että eri ulottuvuudet olisivat ”ortogonaalisia”. Muuten voidaan samanlainen vaikutus saada laajentamalla eri ulottuvuuksissa. Tärkeämpää on kuitenkin, että laajentamismahdollisuudet ovat riittävät ohjelmiston evoluutiotarpeisiin nähden. Yleensä täysi ortogonaalisuus ei ole mahdollinen. Esim. jos järjestelmässä on laajennettavat abstraktiot olioiden sarjallistamiselle ja säilyvyydelle, niillä on luultavasti jotain yhteistä. Riippumattoman laajennettavuuden ulottuvuuksia (jatkoa) Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6 3. 3. 2010 3
Riippumattomasti laajennettavassa järjestelmissä täytyy olla jokin perusrakenne, jota sitten laajennetaan. Eri laajennusulottuvuuksien välisiä rajapintoja kutsutaan pullonkaularajapinnoiksi (bottleneck interfaces). Erityisesti näiden rajapintojen täytyy olla muuttumattomia. Huonosti suunnitellussa komponenttikehyksessä eri ulottuvuuksiin kuuluvat laajennuskomponentit voivat tarvita muitakin yhteyksiä toisiinsa. Ainokaiskonfiguraatio (singleton configuration) Joskus vaaditaan, että järjestelmässä on täsmälleen yksi tai enintään yksi tiettyä ulottuvuutta laajentava komponentti. Esimerkki voisi olla turvallisuudenhallitsin. Rinnakkaisiksi laajennoksiksi sanotaan sellaisia komponentteja, jotka laajentavat samaa ulottuvuutta. Ortogonaalisiksi laajennoksiksi sanotaan sellaisia komponentteja, jotka laajentavat eri ulottuvuuksia. Myös rekursiivinen laajentaminen on mahdollista. Jokin komponentti voi itse olla myös komponenttikehys. Riippumattoman laajennettavuuden ulottuvuuksia (jatkoa) Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6 3. 3. 2010 4
Sopimus (rajapinta ja sen semantiikan määrittely) yhdistää riippumattomasti kehittyviä palvelimia ja asiakkaita. Kun sopimus on julkaistu, sitä ei pitäisi enää muuttaa. Palveluntarjoaja voi lakata tarjoamasta jotakin sopimusta, mutta se menettää silloin ne asiakkaat, jotka tarvitsevat juuri tämän sopimuksen mukaisia palveluja. Palveluntarjoaja ei voi muuttaa sopimuksen spesifikaatiota (semantiikan määrittelyä). Asiakas ei voi muuttaa omaa sopimuksen tulkintaansa, tai sille voi aiheutua virheitä palvelun käytössä. Jotta sopimuksesta voidaan edes keskustella, siihen on pystyttävä viittaamaan yksikäsitteisesti. Yleensä käytetään rajapinnan nimeä viitaamaan koko sopimukseen. Evoluutio vastaan rajapintojen ja sopimusten muuttumattomuusSzyperski, kohta 6.10 Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6 3. 3. 2010 5
Jos sopimusta kuitenkin muutetaan, muutos voi olla kahta laatua: Syntaktinen: rajapinta muuttuu (esim. jonkin metodin parametrien järjestys). Semanttinen: spesifikaatio muuttuu. Monet muutokset koskevat tietysti sekä syntaksia että semantiikkaa, esim. uuden metodin lisääminen. Olio-ohjelmoinnissa palvelinluokka ”hallitsee” sopimusta. Puhutaan ”särkyvän yliluokan” (fragile base class) ongelmasta. Sopimusten evoluutio on mahdollista silloin, kun ne ovat vain jonkin projektin tai organisaation sisäisiä. Kun tehdään muutos, voidaan tarkistaa kaikki asiakkaat ja palvelimet, joihin se vaikuttaa. Joissakin komponenttijärjestelmissä sallitaan rajapintojen versiointi. Entiset asiakkaat voivat käyttää vanhaa versiota, vaikka uudempikin on tarjolla. Esim. entisessä IBM:n SOMissa (System Object Model) uudessa versiossa rajapintaan sai vain lisätä uusia metodeja. Evoluutio vastaan rajapintojen ja sopimusten muuttumattomuus (jatkoa) 3. 3. 2010 6 Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6
Sopimus voi olla määräaikainen. Varsin luonnollista, jos sopimukseen liittyy lisensiointi. Asiakkaat tietävät, että sopimuksen mukaiset palvelut voivat loppua määräpäivänä. Palveluntarjoajat tietävät, että velvollisuus tarjota palveluja loppuu määräpäivänä. Sopimuksen voimassaoloa entisellään voidaan päättää jatkaakin. Voidaan myös tehdä uusi, muunnettu sopimus. Korkeamman tason määräykset voivat vaatia muutoksia sopimuksiin, kuten lainsäädäntö ym. liike-elämän sopimuksiin. Kansalliset ja kansainväliset standardit. Määräävässä asemassa oleva yhtiö. Evoluutio vastaan rajapintojen ja sopimusten muuttumattomuus (jatkoa) 3. 3. 2010 7 Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6
Monimetodit (multimethods, multiple dispatching) Joissakin oliokielissä metodinkutsun dynaaminen sidonta voi perustua myös parametrien eikä vain kohdeolion todellisiin (dynaamisiin) tyyppeihin. Helpottaa asiakkaiden ohjelmointia monissa tilanteissa. Toisaalta aiheuttaa sekä periaatteellisia että toteutuksellisia ongelmia. Kuormitus (overloading) Monissa oliokielissä (esim. C++:ssa, Javassa ja C#:ssa) ja muissa kielissä. Samalla nimellä voi olla useita eri metodeja (samassa luokassa), jotka erotetaan toisistaan parametrien tyyppien perusteella. Oikea vaihtoehto tunnistetaan käännösaikana (salakavala ero monimetodeihin). Kutsutaan joskus ad hoc -polymorfismiksi. Metodeilla olisi hyvä olla samanlainen semantiikka. Muita polymorfismin muotoja 3. 3. 2010 8 Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6
Geneerisyys (genericity) Atk-sanakirja suosittelee myös termiä ”malli-” adjektiivin ”geneerinen” vastineeksi. Myös ”parametrinen polymorfisuus” (parametric polymorphism). Luokalla (tai pakkauksella tms.) voi olla muodollisia geneerisiä parametrejä (malliparametrejä). Varsinainen luokka (tms.) syntyy vasta instantioimalla (generoimalla) geneerinen luokka (luokkamalli) sopivilla todellisilla parametreillä. Instantiointi tapahtuu käännösaikana. Geneeriset parametrit ovat yleensä luokkia (tai muita tyyppejä). Esim. Adassa ja C++:ssa myös tietotyyppien vakioarvot ovat mahdollisia. Syntaktinen geneerisyys C++:ssa geneeriset luokat ja aliohjelmat (templates) ovat makrojen kaltaisia. Mallin tasolla kääntäjä voi todeta vain syntaktiset virheet. Kelvollisuus (mm. tyypitys) ja semantiikka selviävät vasta kutakin instantiointia käännettäessä. Muita polymorfismin muotoja (jatkoa) 3. 3. 2010 9 Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6
Semanttinen geneerisyys Jo mallilla (geneerisellä luokalla tms.) on yksikäsitteinen semantiikka, jonka kääntäjä selvittää. Geneerisen koodin pitää olla kelvollista kaikille mahdollisille todellisille parametreille. Ellei jollekin muodolliselle parametrille määritellä rajoitteita (constraints), sen käyttömahdollisuudet ovat vähäiset. Sopii ensi sijassa säiliö- ja kokoelmaluokille. Oliokielissä tavallisin rajoite on jokin rajaluokka, jonka jälkeläinen todellisen parametrin pitää olla (mm. Eiffelissä ja Javassa). Joissakin kielissä (mm. Eiffelissä ja Javassa) voidaan kääntää jo geneerinen luokka eikä jokaista generoitua luokkaa erikseen. Tämä sekä edellyttää joitakin rajoituksia parametreille että aiheuttaa joitakin rajoituksia generoitujen luokkien käytölle. Perhepolymorfisuus (family polymorphism) Erik Ernstin esittelemä (ECOOP 2001:ssä) uusi lähestymistapa, joka pyrkii yhdistämään periytymisen ja geneerisyyden edut. Vaikuttaa hyvin lupaavalta varsinkin komponenteille. Muita polymorfismin muotoja (jatkoa) 3. 3. 2010 10 Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6
Tässä luvussa tutkitaan perinnän erilaisia merkityksiä oliokielissä ja sitä, missä määrin toteutuksen perinnän sijasta olisi parempi käyttää koostamista (composition). Periytymistä voidaan pitää luokkien välisenä koostamisena, joten tässä verrataan luokkien ja olioiden koostamista toisiinsa. Erityisesti käsitellään särkyvän yliluokan ongelmaa. Kurinalainen periytyminen (disciplined inheritance) voi estää joitakin ongelmia. Juuri tuo oli minun ECOOP’89:ssä julkaistun artikkelini nimi. Siinä pyrin tarkastelemaan perintään koostamisen erikoistapauksena. Työnimenä oli ”Inheritance considered harmful”. Periytymisen kolme tärkeintä aspektia (ainakin Szyperskin mielestä): Toteutuksen perintä eli aliluokkasuhde (subclassing) Rajapinnan perintä eli alityyppisuhde (subtyping) Korvattavuus (substitutability) Perinnän korvaaminen koostamisellaSzyperski, luku 7 3. 3. 2010 11 Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6
Useimmissa oliokielissä toteutuksen ja rajapinnan perintä ovat erottamattomasti yhdessä. Pelkkää rajapintaa ei voida periä sellaisesta luokasta, jossa on yhtään toteutusta mukana. On joitakin kieliä, joissa perintä ja alityyppisuhde on erotettu toisistaan. Erityiset rajapinnanmäärittelykielet (esim. COMissa ja CORBAssa) eivät ota ollenkaan kantaa toteutukseen. Joissakin kielissä on mahdollista kertoa, että aliluokka perii yliluokalta vain toteutuksen. C++:ssa yksityinen perintä – jopa oletuksena! Mahdollisuus on lisätty äskettäin Eiffeliin (non-conforming inheritance). Korvattavuus on näistä ominaisuuksista vaikein määritellä ja toteuttaa. Useimmissa kielissä se tavallaan oletetaan, mutta jää ohjelmoijan vastuulle. C++:ssa ja Eiffelissä aliluokan olio ei voi korvata yksityisesti tai ”mukautumattomasti” perityn yliluokan oliota. Perinnän korvaaminen koostamisella (jatkoa) 3. 3. 2010 12 Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6
Moniperintä (multiple inheritance) Kaksi toisistaan riippumatonta jakoa (ainakin). Peritäänkö rajapinta, toteutus vai molemmat? Onko perittävillä luokilla tai rajapinnoilla yhteisiä esivanhempia (haarautuva moniperintä – fork-join, repeated, diamond inheritance) vai ei (riippumaton moniperintä – independent multiple inheritance) ? Tärkein lähestymistapojen jako: käsitelläänkö kunkin yliluokan uusia (ei perittyjä) ominaisuuksia yhtenä kokonaisuutena (alioliokeskeinen) vai ei (atribuuttikeskeinen)? Useimmat moniperintäkielet noudattavat atribuuttimallia – myös Java rajapintojen periytymisessä. Puhtaassa atribuuttimallissa samaistetaan eri yliluokista perityt atribuutit ja metodit, jos niillä on sama nimi ja tyyppi (tai kutsumuoto). Eiffelissä samaistaminen tapahtuu vain haarautuvassa perinnässä, ja se voidaan estää uudelleennimeämisellä. Tärkein alioliomallia noudattava kieli on C++. Perinnän vivahteitaOsittain Szyperski, kohta 7.2 4. 3. 2010 13 Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6
Atribuuttipohjaisen lähestymistavan lisäjako: otetaanko huomioon koko periytymisrakenne (verkkoperiaate) vai pelkästään välittömät yliluokat (lineaarinen periaate)? Tällä kurssilla pidämme oletuksena verkkoperiaattetta, jota ainakin useimmat staattisesti tyypitetyt kielet noudattavat. Seuraavassa esitetään lineaarisen lähestymistavan erikoisominaisuuksia (hieman yksinkertaistaen). Riippumattomalla ja haarautuvalla moniperinnällä ei ole eroa. Yleensä perintä linearisoidaan niin, että luokan määritelmässä mainittujen yliluokkien järjestys on merkitsevä. Samannimisistä ominaisuuksista peritään automaattisesti vain se, jonka luokka on järjestyksessä ensimmäisenä. Voi aiheuttaa yllättäviä tilanteita mutkikkaissa periytymisverkoissa. Vallitseva (tai ainakin suosittu) Lisp-pohjaisissa oliokielissä: Flavors, CLOS (Common Lisp Object System) ym. Näissä kielissä ei yleensä ole staattista tyypitystä. Perinnän vivahteita (jatkoa) 4. 3. 2010 14 Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6
Riippumaton moniperintä on haarautuvaa yksinkertaisempi tapaus. Useimmissa oliokielissä on kaikkien luokkien yhteinen yliluokka, joka täytyy jättää ottamatta huomioon, jottei kaikkea moniperintää tarvitsisi käsitellä haarautuvana. Atribuuttikeskeisessä periaatteessa vahingossa tapahtuva ominaisuuksien samaistuminen voi kuitenkin aiheuttaa salakavalia virheitä. Uudelleennimeämisen mahdollisuudella voitaisiin sekä korjata tuollaisia tilanteita että sallia erinimistenkin perittyjen ominaisuuksien samaistaminen. Toisaalta moniperintä ei ole sallittu, jos perittävien samannimisten ominaisuuksien tyypit tai kutsumuodot ovat erilaisia – paitsi jos kuormitus on mahdollinen. Javan rajapintojen moniperintä ei ole sallittu, jos perittävien samannimisten metodien kutsumuodoissa eroaa vain tulostyyppi. Jos rajapinta käsittää myös esi- ja jälkiehdot, niiden välillä voi hyvin helposti olla ristiriitoja. Lineaarisessa moniperinnässä samannimisten ominaisuuksien ristiriidat eivät vaikuta, koska vain ensimmäinen niistä otetaan huomioon. Perinnän vivahteita (jatkoa) 4. 3. 2010 15 Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6
Haarautuva moniperintä on vaikeampi ja mielenkiintoisempi tapaus. Nimitys ”vinoneliöperintä” (diamond inheritance) johtuu luonnollisesti luokkakaaviosta. Oli yhtenä aiheena vuoden 1989 ECOOP-artikkelissani. OOPSLA 2009 -konferenssissa oli jälleen yksi artikkeli, jossa esitettiin uutta kielen mekanismia tähän. Ja taistelu jatkuu… Perusongelma: tuleeko kahta eri kautta peritystä ominaisuudesta tai alioliosta aliluokassa yksi vai kaksi? Atribuuttikeskeisessä perinnässä aina yksi, ellei uudelleennimeäminen ole mahdollista. Alioliokeskeisessä perinnässä monistaminen (kaksi alioliota) on helpompi toteuttaa kuin jakaminen (yksi aliolio). C++:ssa oletus, jakaminen virtual-määreellä. Jakavan ja monistavan perinnän yhdistelmät voivat C++:ssa johtaa varsin patologisiin luokkarakenteisiin. Niiden vuoksi ”virtuaalisen” yliluokan tyyppistä viitettä ei voida muuntaa minkään aliluokan tyyppiseksi – yksinkertaisissakaan periytymisrakenteissa. Jakaminen vastaa paremmin periytymisen käsitteellistä merkitystä, varsinkin jos halutaan korvattavuutta. Perinnän vivahteita (jatkoa) 4. 3. 2010 16 Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6
Tilanne on yksinkertaisin, kun peritään vain rajapinta. Mikäli perinnässä sallitaan metodin parametrien tai tuloksen tyypin tai esi- ja jälkiehtojen uudelleenmäärittely, voi kuitenkin aiheutua ristiriita, joka estää jakavan moniperinnän. Atribuutin (aliolioperiaatteessa koko aliolion) haarautuva perintä: Jakava perintä on yhtä ongelmaton kuin rajapinnan tapauksessa. Monistavassa perinnässä korvattavuus kärsii: jos aliluokan oliota käsitellään yhteisen yliluokan oliona, kumpi atribuutti valitaan? Toteutetun metodin haarautuva perintä: Jo jakavassa perinnässä tulee pieni ongelma, jos uudelleenmäärittelyjen takia perittävinä on kaksi eri toteutusta. Useimmat kielet vaativat tällöin uudelleenmäärittelyn aliluokassa. Perittyjen metodien kutsuminen (super)voi helposti aiheuttaa alkuperäisen metodin kutsumisen kahteen kertaan. Monistavassa perinnässä on lisäksi sama ongelma kuin atribuutilla. Perinnän vivahteita (jatkoa) 4. 3. 2010 17 Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6
Lisukkeet (sekoitteet?) (mixins) Jo Flavors-kielessä (Moon, OOPSLA’86) otettiin käyttöön lisukeluokat. Tarkoitettuja täydentämään muita luokkia, eivät voi tuottaa ilmentymiä yksinään. Luonnehdittu myös ”abstrakteiksi aliluokiksi”, mutta niistä peritään ainakin ensi sijassa toteutusta eikä rajapintaa. Dynaamisesti tyypitetyssä kielessä, jossa noudatetaan lineaarista moniperintää, lisukkeiden käyttöön tarvitaan hyvin vähän byrokratiaa. Uusi luokka voi periä mielivaltaisen joukon tavallisia ja lisukeluokkia. Jos luokka perii tai toteuttaa itse ainakin kaikki ne metodit, joita sen omat ja perityt metodit kutsuvat kohdeoliolle, se on kelvollinen konkreetti luokka. Dynaamisessa kielessä tätäkään ei yleensä voida todeta staattisesti; tarkoittaa, että virhettä ”method not found” ei tapahdu ajon aikana. Järkevä semantiikka ei tietenkään toteudu automaattisesti. Perinnän vivahteita (jatkoa) 4. 3. 2010 18 Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6
Esimerkki: Kuvitellaan, että olisi olemassa laajennettu kieli Mixin Java (pitemmälle vietynä kuin kirjassa). Määritellään alkeellinen ikkunaluokka lisukeluokkien avulla. Ensin kolme rajapintaa ikkunan eri aspekteille: interface DrawWindow { void drawWindow (); } interface DrawBorders { void drawBorders (); void handleMouse (Event ev); } interface DrawContents { void drawContents (); Rect getVisibleSection (); Rect getContentsBox (); void scrollTo (Rect newVisible); } Perinnän vivahteita (jatkoa) 5. 3. 2010 19 Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6
Sitten koko ikkunan rajapinta (COMissa tämä voisi olla kategoria): interface Window extends DrawWindow, DrawBorders, DrawContents { } Sitten lisukeluokka kullekin suppealle rajapinnalle: mixin class StdWindowShell implements DrawWindow requires DrawBorders, DrawContents { void drawWindow () { drawBorders (); drawContents (); } } mixin class TextWindowContents implements DrawContents void drawContents () { . . . } Rect getVisibleSection () { . . . } Rect getContentsBox () { . . . } void scrollTo (Rect newVisible) { . . . } } Perinnän vivahteita (jatkoa) 5. 3. 2010 20 Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6
mixin class MotifWindowBorders implements DrawBorders { requires DrawContents { void drawBorders () { . . . // draw title bar Rect visible = getVisibleSection (); Rect total = getContentsBox (); . . . // draw scroll bars } void handleMouse (Event ev); . . . if ("scroll bar thumb moved") { . . . // compute new visible region scrollTo (newVisible); } . . . } } Ainakin luokassa TextWindowContents täytyy tietysti olla myös joitakin atribuutteja, jotta sen metodit voidaan toteuttaa. Perinnän vivahteita (jatkoa) 5. 3. 2010 21 Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6
Perinnän vivahteita (jatkoa) • Lisukeluokan lauseella requireskerrotaan, mitä rajapintoja se vaatii jokaisessa sen perivässä (suoraan tai välillisesti) konkreetissa luokassa olevan toteutettuina. • Kuten komponentin vaatimat rajapinnat. • Tällaista ei tarvita dynaamisissa kielissä. • Lopuksi voidaan luoda konkreetti tavallinen luokka yksinkertaisesti perimällä kaikki lisukkeet. • Yleensä lisukkeita käyttävässä luokassa on omiakin ominaisuuksia. • class MotifTextWindow implements Window • extends StdWindowShell, MotifWindowBorders, • TextWindowContents • { } 5. 3. 2010 22 Komponenttipohjainen ohjelmistotekniikka (Markku Sakkinen) – Osa 6