Olio-ohjelmointi Suunnittelumallit Observer ja State Käyttäytymismallien päätarkoitus on algoritmien toteuttaminen ja vastuiden jakaminen olioiden välillä. Näin ollen käyttäytymismallit kuvaavat luokkien ja olioiden rakenteen lisäksi niiden välisen kommunikoinnin ilmenemismuodot. Ohjelman suorituspolkua on usein vaikea seurata tapauksissa, joihin mallit soveltuvat. Käyttäytymismallit siirtävät huomion suorituspolusta ja antavat ohjelmoijan keskittyä sen sijaan olioiden välisiin suhteisiin. Käyttäytymisluokkamallit jakavat toiminnan eri olioiden vastuulle käyttäen hyväksi perintää. Käyttäytymisoliomalleissa taas sama saadaan aikaan koosteolioita käyttäen. Tässä osassa tutustutaan kahteen oliomalliin: Observer ja State teoksen [Gam] pohjalta. 1. Observer (Tarkkailija) Kun luodaan rakennetta, jossa eri luokkien oliot toimivat yhteistyössä, luokkien välille syntyy usein kytkentöjä. Nämä puolestaan haittaavat luokkien uudelleenkäyttöä. Siitä syystä käytetäänkin erilaisia menetelmiä heikentämään luokkien välisiä kytkentöjä. Yksi tällainen menetelmä on Observer-suunnittelumalli. Tyypillinen tapaus on graafisen käyttöliittymän toteutus ohjelmissa. Jotkin ohjelman luokat käsittelevät sovelluksen dataa ja toiset luokat ohjelman näkymää. Näkymäluokkien välillä on luonnollinen kytkentä, koska niiden on reagoitava datan muutoksiin. Kuitenkin kytkentä halutaan tavallisesti pitää mahdollisimman löyhänä, jotta luokkia voitaisiin käyttää uudelleen. Näkymäluokkien on käyttäydyttävä kuin ne tietäisivät toisistaan, vaikka todellisuudessa näin ei ole. Tämä voidaan toteuttaa Observer-suunnittelumallilla. Suunnittelumallin keskeiset toimijat ovat kohde (subject) ja tarkkailija (observer). Näiden välillä vallitsee ns. julkaise-tilaa-suhde (publish-subscribe). Yhtä kohdetta voi seurata useampi tarkkailija, ne rekisteröityvät tietyn kohteen tarkkailijoiksi ja saavat tällöin ilmoituksen kohteen muutoksista. Yleensä ilmoitetaan ainoastaan siitä, että kohde on muuttunut. Tarkkailijan on tällöin tiedusteltava kohteen tilaa ja päivitettävä itsensä. Tyypillinen Observer-mallin mukainen tilanne esitettiin jo suunnittelumallien johdanto-osassa MVC-mallin yhteydessä Kuva 1. Esimerkki Observer-mallin mukaisesta tilanteesta (Lähde: [Gam]) 1
Observer-malli määrittelee näin ollen olioiden välille yksi-moneen riippuvuussuhteen siten, että muutos ensimmäisen olion tilassa aiheuttaa automaattisesti ilmoituksen siitä riippuville olioille. Nämä oliot päivitetään ilmoitettaessa. Edellä olevan tarkastelun perusteella malli soveltuu käytettäväksi, kun halutaan pitää riippuvuussuhde kahden ohjelmassa esiintyvän abstraktion välillä löyhänä kapseloimalla niiden toiminnat eri luokkiin. Toinen käyttökohde ilmenee, kun ohjelman käyttämistä olioista yhden muuttaminen aiheuttaa muutoksen oliojoukossa, jonka kokoa ei ennalta tunneta. Mallia voi käyttää myös, kun oliossa tapahtuvien muutoksen halutaan välittyvän joukolle olioita, joiden identiteettiä ei ennalta tiedetä. Mallin luokkarakenne on esitetty seuraavassa kaaviossa Mallin toimijat ovat Kuva 2. Observer-mallin luokkarakenne (Vrt. [Gam]) Subject (kohde), joka tuntee tarkkailijansa. Tarkkailijoiden määrää ei ole rajoitettu. Tarjoaa rajapinnan tarkkailijoiden lisäämiseksi ja poistamiseksi. Observer (tarkkailija) määrittelee rajapinnan olioille, joille on ilmoitettava kohteen muutoksista. ConcreteSubject säilyttää tilan, jota ConcreteObserver tarkkailee. Lähettää tarkkailijoilleen ilmoituksen tilansa muuttuessa. ConcreteObserver sisältää tilan, joka on pidettävä ConcreteSubjectin tilaa vastaavana. Toteuttaa Observerin määrittelemän rajapinnan tämän aikaansaamiseksi. Observer sallii kohteiden ja tarkkailijoiden muokkaamisen toisistaan riippumatta. Toisin sanoen kohteita voi käyttää uudelleen käyttämättä tarkkailijoita ja päinvastoin. Samoin tarkkailijoita voidaan lisätä tekemättä muutoksia kohteisiin tai muihin tarkkailijoihin. Kohteiden ja tarkkailijoiden välinen suhde on löyhä: Ainoa asia, jonka kohteen tarvitsee tietää tarkkailijoista, on että ne toteuttavat yhteisen yksinkertaisen rajapinnan. Huomaa, että ilmoitus on aina broadcast-tyyppinen: se lähtee kaikille rekisteröityneille tarkkailijoille; on tarkkailijan vastuulla jättää ilmoitus huomiotta tarvittaessa. 2
Protokollan yleisyys ja yksinkertaisuus voi myös aiheuttaa ongelmia. Tarkkailijat eivät tiedä toisistaan, joten ne eivät voi myöskään tietää kohteen muuttamisen seurauksia. Näin ollen jotkin kohteeseen vaikuttavat operaatiot voivat aiheuttaa tarkkailijoiden päivitysten kasaantumisen. Protokollassa ei myöskään määritellä, että tarkkailijoille pitäisi ilmoittaa, mistä kohteen muuttuminen johtui. Tämän selvittäminen jää tarkkailijoiden vastuulle. Esimerkki. Esitetään periaatteellinen C++-toteutus mallille. Observer-rajapinta ja Subjectluokka voitaisiin toteuttaa seuraavasti. // Tarkkailijoiden kantaluokka class Observer { virtual ~Observer(){; virtual void update(subject* changedsubject) = 0; ; class Subject { virtual ~Subject(){; virtual void attach(observer*); virtual void detach(observer*); virtual void notify(); private: std::list<observer*> observers; ; // Tarkkailijan lisääminen void Subject::attach (Observer* o) { observers.push_back(o); // Tarkkailijan poistaminen void Subject::detach (Observer* o) { observers.remove(o); // Tarkkailijoille ilmoittaminen void Subject::notify () { std::list<observer*>::iterator iter; for (iter = observers.begin(); iter!= observers.end(); iter++) { (*iter)->update(this); 3
Edellä on tarkkailijoiden säiliönä käytetty C++:n kokoelmaa list, josta alkio voidaan poistaa sen remove-metodilla. Toteutuksessa luokka Subject perittäisiin johonkin hyödylliseen luokkaan, joka ilmoittaa tarkkailijoilleen: class ConcreteSubject : public Subject { ; // Luokan koodia // Metodi joka aiheuttaa ilmoituksen tarkkailijoille void changestate(); void ConcreteSubject::changeState(){ // Muutos olion tilassa notify(); Tarkkailijat perivät abstraktin Observer-luokan: class ConcreteObserver : public SomeClass, public Observer { // Luokan koodia ; // Metodi, joka suoritetaan, kun saadaan ilmoitus void update(subject *changedsubject); void ConcreteObserver::update(Subject *changedsubject){ // Kysytään parametriolion tila ja // tehdään tarvittavat operaatiot Käsitellään seuraavaksi hieman mallin toteuttamista. Yleensä tarkkailtava olio säilyttää listaa tarkkailijoistaan jossakin tietorakenteessa, esimerkiksi edellä käytetään C++:n standardikirjaston list-kokoelmaa. Jos tarkkailijoita on vähän ja tarkkailtavia paljon, voidaan tilan säästämiseksi tehdä jokin assosiatiivinen hakumenetelmä. Voidaan esimerkiksi käyttää hash-taulukkoa tallentamaan tarkkailijoita. Useamman kohteen tarkkailu onnistuu, kun tarkkailtava olio välittää viitteen itseensä update-metodin parametrina, kuten edellisessä esimerkkitoteutuksessa. Tyypillisesti notify-kutsu tehdään, kuten edellisessä esimerkkitoteutuksessakin, tarkkailtavan olion tilaa muuttavissa metodissa. Metodia voivat myös kutsua tarkkailtavan olion asiakkaatkin. Tämä voi olla tehokkaampaa, koska asiakas voi tehdä tarkkailtavaan olioon useita muutoksia ja vasta sitten kutsua notify-metodia. Tämä ratkaisu on hieman epävarmempi, koska tällöin notifykutsut unohtuvat helpommin. Jos ne tehdään jokaisessa tilaa muuttavassa metodissa, tarvittavat notify-kutsut suoritetaan varmasti. Mikäli tarkkailtavia olioita voidaan poistaa, tästä pitää ilmoittaa tarkkailijoille, joiden on reagoitava ilmoitukseen asianmukaisesti. Muuten tarkkailijaan voi jäädä esimerkiksi viitteitä tuhottuihin olioihin. Ilmoituksen käsitteleminen toteutetaan usein niin, että ilmoituksen saaja tiedustelee ilmoittajan tilaa ja toimii sitten tämän tilan mukaisesti. Siksi on varmistettava, että kohteen sisäisen tila on kunnossa ennen ilmoitusta. 4
Kielissä, jotka eivät tue moniperiytymistä, voi mallin toteutus olla joissakin tapauksissa hankalaa. Esimerkiksi Smalltalkissa onkin kaikkien luokkien kantaluokassa sekä tarkkailijan että tarkkailtavan rajapinta. Näin ollen jokainen olio voi toimia sekä tarkkailijana että tarkkailtavana. Observer-mallin yleiskäyttöisyys ilmenee siitäkin, että myös Java-kieli tukee suoraan Observersuunnittelumallia, Javassa on määritelty rajapinta Observer, jossa on yksi metodi void update(observable o, Object arg) Tätä metodia kutsutaan, kun tarkkailtava kutsuu ilmoitusmetodiaan. Tarkkailtaville olioille on määritelty luokka Observable, jolla on mm. seuraavat metodit public void addobserver(observer o) Lisää tarkkailijan siihen joukkoon, joille ilmoitetaan. public void deleteobserver(observer o) Poistaa tarkkailijan ilmoituksen saajien joukosta. public void notifyobservers() Ilmoittaa kaikille tarkkailijoille public void notifyobservers(object arg) Ilmoittaa kaikille tarkkailijoille ja lähettää parametrin arg protected void setchanged() Asettaa tarkkailtavan olion muutettuun tilaan protected void clearchanged() Poistaa tarkkailtavan olion muutetun tilan Näitä luokkia käyttämällä voidaan toteuttaa Observer-mallia käyttäviä ohjelmia. 5
2. State State-suunnittelumallia käytetään, kun halutaan luoda luokka, joka muuttaa käyttäytymistään sisäisen tilansa mukaan. Usein kohdataan tilanne, jossa luokan käyttäytyminen muuttuu sen muuttujien arvojen konfiguraation mukaan. Tämä johtaa monesti koodiin, jossa on mutkikkaita ehto- ja valintarakenteita. State-suunnittelumallin avulla tällaisista pyritään pääsemään eroon. Tilanne korjataan niin, että luokka sisältää jäseninään usean erityyppisen luokan olioita ja pyyntö välitetään tilanteen mukaan sillä hetkellä aktiiviseen osaolioon. Suunnittelumallin tarkoitus on siis luoda olio, jonka käyttäytyminen muuttuu sen sisäisen tilan mukaan niin, että olio ikään kuin näyttää vaihtavan luokkaansa. Otetaan esimerkiksi palvelinsovellus, joka palvelee yhtä asiakasta kerrallaan. Tällöin palvelin voi olla kolmessa tilassa: 1. Kuuntelemassa asiakkaiden palvelupyyntöjä. 2. Palvelemassa asiakasta 3. Suljettu Jokaisessa tilassa palvelin vastaa pyyntöihin (esimerkiksi palvelun aloittamispyyntö ja lopettamispyyntö) eri tavalla. Palvelimen tilakaavio voisi olla esimerkiksi Palvelinluokka voidaan tällöin suunnitella seuraavasti: Kirjoitetaan erillinen abstrakti tilaluokka, jonka aliluokkia ovat eri tilaluokat, jotka toteuttavat palvelupyynnöt sopivasti. Palvelinluokka puolestaan pitää jäsenmuuttujanaan yllä kuhunkin tilaan sopivaa tilaluokan oliota. Näin palvelimelle osoitettu pyyntö ohjautuu aina oikean tyyppiselle tilaoliolle, joka suorittaa pyynnön. Suunnittelumalli soveltuu luontaisesti kahteen tapaukseen: 1. Olion käyttäytyminen riippuu sen tilasta ja tätä tilasta riippuvaa käyttäytymistä on muutettava ajon aikaisesti. 2. Luokan koodissa on laajoja valintarakenteita, joissa ohjataan olion toimintaa sen tilan mukaan. Näistä voidaan päästä eroon siirtämällä rakenteiden toiminta sopivasti valittuun luokkarakenteeseen. Mallin luokkarakenne on kuvattu seuraavassa: 6
Kuva 3. State-mallin luokkarakenne (Vrt. [Gam]) Mallin toimijat ovat Context, joka määrittelee asiakkaihin päin näkyvän rajapinnan ja pitää yllä jäsenmuuttujassaan kulloisenkin tilan kuvaavaa State-oliota. State, joka on kaikkien tilaluokkien kantaluokka. ConcreteStateA, jne. Luokat, jotka toteuttavat kaikki mahdolliset tilat. Esimerkki. Edellä mainitun palvelinsovelluksen perusta voisi olla C++:lla toteutettuna seuraava. Palvelinluokan koodin pohja olisi: class Palvelin { private: // Tämänhetkinen tila PalvelinState* pscurrent; // Mahdolliset tilat PalvelinState *pslisten; PalvelinState *psserving; PalvelinState *psclosed; // Vakiot tilanvaihdoille const static int LISTEN = 1; const static int SERVING = 2; const static int CLOSED = 3; ; Palvelin(); ~Palvelin(); void startservice(); void stopservice(); void closeserver(); void changestate(int to); Palvelin::Palvelin(){ pslisten = new ListenState(); psserving = new ServingState(); psclosed = new ClosedState(); pscurrent = pslisten; 7
Palvelin::~Palvelin(){ delete pslisten; delete psserving; delete psclosed; void Palvelin::startService() { pscurrent->start(*this); void Palvelin::stopService() { pscurrent->stop(*this); void Palvelin::closeServer() { pscurrent->close(*this); void Palvelin::changeState(int to) { switch(to) { case LISTEN: pscurrent = pslisten; break; case SERVING: pscurrent = psserving; break; case CLOSED: pscurrent = psclosed; break; Tilojen perusluokka voitaisiin toteuttaa abstraktina luokkana class PalvelinState { virtual void start(palvelin& p)=0; virtual void stop(palvelin& p)=0; virtual void close(palvelin& p)=0; virtual ~PalvelinState(){ ; 8
jolloin tilaluokkien koodit voisivat perustua alla oleviin runkoihin class ListenState: public PalvelinState { // Määrittely kuten luokassa PalvelinState void ListenState::start(Palvelin& p) { std::cout << "Started service..." << std::endl; p.changestate(palvelin::serving); void ListenState::stop(Palvelin& p){ std::cout << "Service not started. Cannot stop." << std::endl; void ListenState::close(Palvelin& p){ std::cout << "Closing server." << std::endl; p.changestate(palvelin::closed); class ServingState: public PalvelinState { // Määrittely kuten luokassa PalvelinState void ServingState::start(Palvelin& p) { std::cout << "Service already started." << std::endl; void ServingState::stop(Palvelin& p){ std::cout << "Service stopped." << std::endl; p.changestate(palvelin::listen); void ServingState::close(Palvelin& p){ std::cout << "Cannot close while service active." << std::endl; class ClosedState: public PalvelinState { // Määrittely kuten luokassa PalvelinState void ClosedState::start(Palvelin& p) { std::cout << "Cannot start, server closed." << std::endl; void ClosedState::stop(Palvelin& p){ std::cout << "Cannot stop, server closed." << std::endl; void ClosedState::close(Palvelin& p){ std::cout << "Server already closed." << std::endl; Käytettäessä State-suunnittelumallia on otettava huomioon sen seurauksia. Koska malli kapseloi tiettyyn tilaan liittyvän toiminnan yhteen luokkaan, on helppoa lisätä uusia tiloja laatimalla uusia aliluokkia. Tämä lähestymistapa on sujuvampi kuin se, että tilaa pidettäisiin yllä Context-oliossa, jolloin luokan operaatiot voisivat tarkistaa tilan ja muuttaa sitä. Tällöin ajaudutaan helposti kirjoittamaan laajoja valintarakenteita. State-malli poistaa tämän ongelman sen kustannuksella, että Contextin toiminta hajautuu useaan luokkaan. 9
Mallia käytettäessä Context-olion tilamuutokset on helpompi nähdä koodista, koska jokainen tilamuutos perustuu tilaolion vaihtamiseen. Mikäli olion tila perustuu pelkästään paikallisten muuttujien arvoihin, voi erilaisten pysyvien tilojen hahmottaminen olla vaikeampaa. Lisäksi tilaoliot voidaan myös jossain tapauksissa jakaa, ellei niillä itsellään ole sisäistä tilaa. Käsitellään vielä lopuksi State-mallin toteuttamista. Edellä olevassa palvelinesimerkissä tilaoliot määräävät palvelinolion tilanmuutoksen. Tämä onkin joustavampi tapa kuin jättää tilan muuttaminen mallin Context-olion vastuulle. Nyt nimittäin tilanmuutoslogiikan muuttaminen ja uusien tilaolioiden lisääminen on huomattavasti helpompaa kuin keskitetyssä tapauksessa. Hajautetussa järjestelmässä on kuitenkin pari haittaa: Context-olioon joudutaan määrittelemään rajapinta, jonka kautta tilaa voidaan muuttaa ja tilaolioiden kytkentä kasvaa, koska niiden on tiedettävä toisistaan niin paljon, että ne voivat aiheuttaa tilanmuutoksen. Erityisesti kielissä, joissa ei ole roskienkeruuta, pitää miettiä, millä tavoin tilaoliot luodaan. Ne voidaan luoda kaikki kerralla ja pitää olemassa koko ohjelman suorituksen ajan. Toinen mahdollisuus on luoda oliot tarvittaessa ja hävittää ne, kun niitä ei enää tarvita. Jälkimmäinen vaihtoehto saattaa kannattaa, kun tilaoliot ovat suuria ja tilanvaihtoja tapahtuu harvoin. Sen sijaan tilojen vaihtuessa tiuhaan, kannattaa valita ensimmäinen vaihtoehto. Mainittakoon vielä, että State-malli voidaan korvata käyttämällä taulukkoja. Tällöin tilasta toiseen siirryttäessä seuraava tila luetaan taulukosta. Tämä lähestymistapa korostaa mieluummin tilasiirtymiä, kun State-malli taas keskittyy enemmän tilakohtaiseen käyttäytymiseen. Lähteet [Gam] Gamma, Helm, Johnson, Vlissides: Design Patterns: Elements of Reusable Object- Oriented Software, Addison-Wesley 1995 10