Olio-ohjelmointi Suunnittelumallit Proxy, Factory Method, Prototype ja Singleton Tässä osassa tutustutaan yhteen rakennemalliin (Proxy) ja kolmeen luontimalliin (Factory Method, ) teoksen [Gam] pohjalta. Luontimallit kuvaavat epäsuoria tapoja luoda olioita ohjelmassa. Niiden avulla järjestelmä voidaan tehdä riippumattomaksi siitä, miten sen oliot luodaan ja esitetään sekä miten niitä yhdistellään. Luokkamallit perustavat toimintansa perintään, kun oliomallit delegoivat olion luomisen toisille olioille. Luontimalleja tarvitaan erityisesti, kun ohjelmassa siirrytään käyttämään koosteolioita perimisen asemasta. Tällöin sopivien olioiden luominen voi vaatia mutkikkaampia toimenpiteitä kuin pelkkä alustaminen. 1. Proxy (Edustaja) Kun ohjelmassa luodaan erittäin massiivisia olioita, niitä ei kannattaisi pitää muistissa koko ohjelman suoritusaikaa vaan luoda ne tarvittaessa ja tuhota käytön jälkeen. Entä jos tällainen olio on osana toisessa oliossa? Isäntäolion rakenteen vuoksi suuri osaolio voidaan joutua luomaan ennenaikaisesti. Tällaisessa tapauksessa voidaan käyttää sijaisoliota, jonka rajapinta on sama kuin suuren olion ja joka huolehtii suuren olion luomisesta, kun sitä tosiasiassa tarvitaan. Tästä toimintatavasta syntyvää suunnittelumallia sanotaan Proxyksi eli Edustajaksi. Esimerkki. Dokumentissa voi esiintyä elementtinä suuri kuva. Sen sijaan, että kuva luettaisiin heti muistiin dokumenttia luotaessa, voidaan luoda kuvan tiedostonimellä tunnistettava Proxyolio, joka lisätään dokumenttiin. Proxy-olio luo kuvaolion vasta, kun dokumenttiolio näyttää kuvan. Proxy-mallia voidaan käyttää kaikissa tapauksissa, kun olioon on viitattava muuten kuin tavallisen suoran viitteen kautta. Etäedustajaa (remote proxy) käytetään viittamaan olioon, joka sijaitsee toisessa muistialueessa. Virtuaaliedustaja (virtual proxy) luo suuria olioita tarvittaessa, kuten aiemmassa esimerkissä Suojausedustaja (protection proxy) kontrolloi pääsyä olioon. Sitä voidaan käyttää erityisesti, kun on toteutettava erilaisia pääsyoikeuksia. Älykkäät osoittimet ovat myös proxyja. Älykäs osoitin on viite, johon on toteutettu muitakin toimintoja kuin pelkkä olioon viittaaminen. Se voi hoitaa esimerkiksi oliolle varatun muistin vapauttamisen, kun siihen ei enää ole viitteitä. 1
Mallin luokkarakenne on esitetty alla Kuva 1. Proxy-mallin luokkarakenne (Vrt. [Gam]) Mallista syntyvä oliorakenne voisi olla esimerkiksi Kuva 2. Proxy-mallin oliokaavio (Vrt. [Gam]) Mallin osallistujat ovat Proxy, joka säilyttää viitteen todelliseen kohdeolioon. Proxyn rajapinta on sama kuin kohdeolion, jotta sitä voidaan käyttää kohdeolion sijasta. Kontrolloi pääsyä todelliseen kohteeseen ja voi olla vastuussa sen luomisesta ja tuhoamisesta. Proxylla voi tyypistään riippuen olla myös muita vastuita. Kohde (Subject) määrittelee todellisen kohteen ja Proxyn yhteisen rajapinnan, jotta Proxya voidaan käyttää kaikkialla, missä todellista kohdetta oletetaan käytettävän. Todellinen kohde (RealSubject); kohde jonka sijaisena Proxy toimii. Proxy-mallin käyttämällä epäsuoralla olioon viittaamisella saavutetaan monia etuja, mm. voidaan piilottaa olion käyttäjiltä se, että olio sijaitsee toisessa osoiteavaruudessa. Edelleen voidaan tehdä optimointeja oliota käsiteltäessä, kuten luoda suuria olioita tarvittaessa. Varsinkin kielissä, joissa ei ole automaattista roskien keruuta, erilaiset Proxy-oliot voivat automatisoida muistin vapautuksen ja mahdolliset muut siivousoperaatiot olioita käytettäessä. 2
2. Factory Method (Tehdasmetodi) Moniin sovelluksiin liittyy jonkinlainen dokumenttiolio. Tällöin ohjelmassa esiintyy kaksi perusoliota: sovellusolio ja dokumenttiolio. Näiden perusluokat ovat yleensä abstraktit ja kukin sovellus vastaa konkreettisesta toteutuksesta. Näin ollen sovellusolio on vastuussa dokumentin luomisesta, mutta se ei tunne kuin dokumenttien abstraktin perusluokan eikä tiedä, minkälainen dokumentti on luotava. Tämä on luonnollisesti ongelmallista. Factory Method tarjoaa ratkaisun ongelmaan. Mallissa tehdään jokaista luotavaa dokumenttioliota vastaava sovellusluokan aliluokka. Tällöin sovellusluokan dokumentin luovan metodin kutsuminen ohjautuu dynaamisen sidonnan avulla oikealle aliluokalle, joka osaa luoda oikeantyyppisen dokumentin. Tämä tilanne voidaan luonnollisesti abstrahoida sovellusdokumenttiyhteydestä yleiseksi suunnittelumalliksi. Factory Method-suunnittelumallia käytetään, kun luokassa on luotava olioita, joiden tarkkaa tyyppiä ei tunneta ja halutaan antaa vastuu olioiden luomisesta aliluokille. Mallia voidaan käyttää myös, kun luokat jakavat vastuita useille avustavilla aliluokille ja halutaan säilyttää paikallisena tieto siitä, mitä apuluokkaa käytetään. Mallin luokkarakenne on esitetty seuraavassa kaaviossa: Kuva 3. Factory Method-mallin luokkakaavio (Vrt. [Gam]) Mallin osallistujat ovat Product, joka määrittelee FactoryMethodin luomien olioiden rajapinnan. ConcreteProduct toteuttaa Productin rajapinnan. Creator esittelee tehdasmetodin (FactoryMethod), joka palauttaa Product-tyyppisen olion. Voi myös määritellä oletustoteutuksen. Voi itse kutsua tehdasmetodia. ConcreteCreator uudelleenmäärittelee tehdasmetodin luodakseen Product-olion. Suunnittelumallia käyttämällä poistetaan tarve liittää ohjelmaan sovelluskohtaisia luokkia. Ohjelmassa käsitellään ainoastaan Product-rajapintaa, joten sovellus toimii minkä tahansa sen perivän konkreettisen luokan kanssa. Mallin käyttäminen voi kuitenkin johtaa monimutkaiseen luokkarakenteeseen, koska asiakasolioiden täytyy ehkä periä Creator-luokka luodakseen jonkin tietyn Product-luokan aliluokan olion. 3
Tehdasmetodia ei tarvitse välttämättä kutsua Creator-luokista. Kutsu voidaan tehdä myös ulkopuolisesta luokasta. Tällä menetelmällä voidaan kytkeä rinnakkaiset luokkarakenteet toisiinsa. Käsitellään lyhyesti vielä suunnittelumallin toteutusta. Voidaan käyttää kahta versiota: 1. Luokka Creator on abstrakti eikä tarjoa toteutusta tehdasmetodille 2. Luokka Creator on konkreettinen ja siinä on oletustoteutus tehdasmetodille. Ensimmäisessä tapauksessa Creator-luokan aliluokkien on pakko määritellä toteutus tehdasmetodille. Tätä menetelmää käytetään, kun Creator-luokan avulla luodaan olioita ennalta tuntemattomista luokista. Toinen tapaus on käytössä lähinnä mukavuussyistä: halutaan joustavuutta olioiden luomiseen. Tehdasmetodit voidaan myös parametrisoida. Tällöin luokan tehdasmetodi voi luoda useanlaisia olioita, kunhan ne toteuttavat Product-rajapinnan. Aiemmin mainitussa dokumenttiesimerkissä yksi sovellus voisi tukea monenlaisia dokumentteja; dokumentin tyyppi valittaisiin tehdasmetodille syötetyn parametrin perusteella. Normaalisti jokaista Product-luokkaa kohden kirjoitetaan Creator-luokan aliluokka. Tämän voi joskus välttää käyttämällä luokkamalleja. Ratkaisu onnistuu C++-kielessä ja Javan uudemmissa versioissa. 3. Prototype (Prototyyppi) Ohjelmaa laadittaessa voidaan kohdata tilanne, jossa luokan tulisi luoda olio, mutta luokka ei osaa tehdä tätä. Tämä voi sattua esimerkiksi laadittaessa graafista editoria, jolla lisätään dokumenttiin erityyppisiä olioita, joita käsitellään dokumentissa. Lisäämisestä vastaava luokka ei välttämättä osaa kuitenkaan luoda kaikkia tarvittavia olioita. Yksi ratkaisu tähän ongelmaan on soveltaa Prototype-suunnittelumallia: jokaisesta lisättävästä oliotyypistä on mallikappale, josta tehdään tarvittaessa kopio. Luotu kopio lisätään dokumenttiin. Suunnittelumallia voidaan käyttää kun järjestelmä ei saisi riippua siitä, miten sen tuottamat oliot luodaan ja kun jokin seuraavista ehdoista on voimassa: 1. Luotavan olion tyyppi määräytyy ajonaikaisesti, 2. Halutaan välttää Factory Method-suunnittelumallin tehdasluokkien rakennetta, 3. Luotavan luokan olioilla voi olla hyvin harvoja laillisia tiloja. Tällöin voi olla kätevämpää luoda olio kopioimalla kuin alustamalla suoraan. 4
Prototype-mallin luokkarakenne on seuraava Kuva 4. Prototype-mallin luokkakaavio (Vrt. [Gam]) Mallin osallistujat ovat Prototype, joka määrittelee luotavien olioiden rajapinnan. ConcretePrototype toteuttaa metodin itsensä kopioimiseksi. Client luo uusia olioita kutsumalla Prototype-luokan clone-metodia. Mallin perusidea on sama kuin Factory Method-mallissakin: on luotava olioita, joiden luomisen yksityiskohtia ei tunneta. Lisäksi Prototype-mallilla saavutetaan joitakin lisäetuja. Prototypemallilla saavutetaan yleensä yksinkertaisempi luokkarakenne kuin Factory Method-mallilla, koska olion luomiseen ei tarvita erillisiä luokkia. Lisäksi malli on joustavampi kuin Factory Method, koska uusia prototyyppejä voidaan tuoda ohjelmaan vaikka ajon aikana, riittää, että ne toteuttavat Prototype-rajapinnan. Lisäksi samasta luokasta voidaan tehdä useita prototyyppejä, joista voidaan luoda kopioita. Mallin suurin rajoitus on vaadittu rajapinta: kaikkien kopioitavien olioiden on toteutettava clonemetodi. Tämä voi olla vaikeata, jos yritetään soveltaa mallia luomaan jo olemassa olevan luokkarakenteen olioita. Mallin toteutuksen hankalin osa on clone-operaation toteuttaminen. Kloonattaessa on kuitenkin päätettävä, käytetäänkö pinta- vai syväkopiota. Pintakopiossa kopio-olioon kopioidaan sellaisenaan kopioitavan olion attribuuttien arvot. Tämä voi kuitenkin olla virheratkaisu, jos olio koostuu osoittimista varattuun muistiin tai dynaamisesti varattuihin olioihin. Java-kielessä olioihin viitataan aina viitetyyppisellä muuttujalla, joten Javassa ongelma tulee esiin aina koosteolioita käytettäessä. Näissä tapauksissa on mietittävä, pitääkö myös osaolioista tehdä kopiot uuden olion osaolioiksi, jolloin tehdään syväkopio. Pintakopiossa kopioitaisiin vain viitteet osaolioihin. Monissa kielissä on suora tuki olion kloonaamiseen. C++:ssa käytetään kopiomuodostinta, Javassa puolestaan kloonattavien olioiden on toteutettava Cloneable rajapinta, jolloin voidaan käyttää Object-luokan clone()-metodia. Se tekee oliosta pintakopion. 5
Joskus halutaan ohjelmassa kopio-oliosta ainoastaan tieto sen luokasta, jonka perusteella tehdään uusi olio oletusasetuksilla. Tällöin mallia voidaan laajentaa niin, että clone-operaation lisäksi Prototype-rajapintaa lisätään metodi create(), joka tuottaa aina oletusolion. 4. Singleton (Ainokainen) Joskus tarvitaan luokka, jonka instansseja voi kerrallaan olla olemassa korkeintaan yksi. Esimerkiksi ohjelmassa käytettyjen säiliöluokkien instanssien määrää voi olla hyvä rajoittaa yhteen. Kun olio on luotu, sen on myös syytä olla helposti saatavilla eri puolilla ohjelmaa. Globaalin muuttujan käyttäminen ei ole yleensä hyvä ratkaisu: on järkevämpää antaa luokan itsensä ratkaista ongelma. Luokka rajoittaa instanssinsa yhteen ja tarjoaa pääsyn ainoaan olioonsa. Tämä on Singleton-suunnittelumalli. Mallia siis sovelletaan tapauksiin, joissa on rajoitettava luokan instanssien lukumäärä yhteen ja luotu instanssi on saatava julkisen rajapinnan kautta. Luokka on voitava myös periä aliluokkiin niin, että luokan asiakkaiden ei tarvitse muuttaa koodiaan. Mallin luokkarakenne on yksinkertainen Kuva 5. Singleton-mallin luokkakaavio (Vrt. [Gam]) Luokkakaaviosta paljastuu, että luokan instanssi saadaan kutsumalla luokkakohtaista metodia getinstance. Tämä palauttaa luodun instanssin, jos instanssi on jo luotu. Jos instanssia ei ole vielä olemassa, se luodaan ja palautetaan. Olion suora luominen on myös estettävä, esimerkiksi kirjoittamalla sille private-tyyppinen muodostin, jotta olioita ei voi luoda luokan ulkopuolelta. Samoin on syytä yleensä estää myös olion kopioiminen, jotta luodusta oliosta ei voi tehdä uusia kopioita. C++-kielessä kopioinnin voi estää esittelemällä (private-tyyppinen) kopiointimuodostin, mutta jättämällä se toteuttamatta. 6
Mallin käytöllä saavutetaan monia etuja. Singleton-luokka voi kontrolloida muiden olioiden pääsyä instanssiinsa, koska se kapseloi instanssin sisäänsä. Mallia käytettäessä ei tarvita globaalia muuttujaa käyttämään Singleton-oliota: riittää, että luokan rajapinta on käytettävissä. Singleton-luokka voidaan myös periä ja käyttää aliluokkaa. Valinta voidaan tehdä ajonaikaisesti. Mallia voidaan myös helposti muokata niin, että luokan olioita voidaan luoda jokin muu kiinteä määrä kuin yksi. Lähteet [Gam] Gamma, Helm, Johnson, Vlissides: Design Patterns: Elements of Reusable Object- Oriented Software, Addison-Wesley 1995 7