Olio-ohjelmointi Suunnittelumallit Adapter ja Composite. 1. Adapter



Samankaltaiset tiedostot
812347A Olio-ohjelmointi, 2015 syksy 2. vsk. VII Suunnittelumallit Adapter ja Composite

812347A Olio-ohjelmointi, 2015 syksy 2. vsk. IX Suunnittelumallit Proxy, Factory Method, Prototype ja Singleton

Olio-ohjelmointi Suunnittelumallit Proxy, Factory Method, Prototype ja Singleton. 1. Proxy (Edustaja)

815338A Ohjelmointikielten periaatteet Harjoitus 5 Vastaukset

812347A Olio-ohjelmointi, 2015 syksy 2. vsk. VIII Suunnittelumallit Observer ja State

Mikä yhteyssuhde on?

JAVA-PERUSTEET. JAVA-OHJELMOINTI 3op A JAVAN PERUSTEET LYHYT KERTAUS JAVAN OMINAISUUKSISTA JAVAN OMINAISUUKSIA. Java vs. C++?

Opintojakso TT00AA11 Ohjelmoinnin jatko (Java): 3 op Rajapinnat ja sisäluokat

Oliosuunnitteluesimerkki: Yrityksen palkanlaskentajärjestelmä

15. Ohjelmoinnin tekniikkaa 15.1

812341A Olio-ohjelmointi Peruskäsitteet jatkoa

Opintojakso TT00AA11 Ohjelmoinnin jatko (Java): 3 op Taulukot & Periytyminen

T Olio-ohjelmointi Osa 5: Periytyminen ja polymorfismi Jukka Jauhiainen OAMK Tekniikan yksikkö 2010

1 Tehtävän kuvaus ja analysointi

Rajapinta (interface)

TIE Ohjelmistojen suunnittelu

15. Ohjelmoinnin tekniikkaa 15.1

Metodien tekeminen Javalla

Ohjelmointi 2 / 2010 Välikoe / 26.3

Olion elinikä. Olion luominen. Olion tuhoutuminen. Olion tuhoutuminen. Kissa rontti = null; rontti = new Kissa();

Harjoitus Olkoon olemassa luokat Lintu ja Pelikaani seuraavasti:

JAVA-OHJELMOINTI 3 op A274615

Ohjelmoinnin jatkokurssi, kurssikoe

812341A Olio-ohjelmointi, IX Olioiden välisistä yhteyksistä

4. Luokan testaus ja käyttö olion kautta 4.1

Poikkeusten ja tapahtumien käsittely

1. Olio-ohjelmointi 1.1

Olio-ohjelmointi Javalla

Sisällys. JAVA-OHJELMOINTI Osa 6: Periytyminen ja näkyvyys. Luokkahierarkia. Periytyminen (inheritance)

Sisällys. JAVA-OHJELMOINTI Osa 7: Abstrakti luokka ja rajapinta. Abstraktin luokan idea. Abstrakti luokka ja metodi. Esimerkki

9. Periytyminen Javassa 9.1

Rajapinnasta ei voida muodostaa olioita. Voidaan käyttää tunnuksen tyyppinä. Rajapinta on kuitenkin abstraktia luokkaa selvästi abstraktimpi tyyppi.

9. Periytyminen Javassa 9.1

Ohjelmistotekniikan menetelmät, suunnittelumalleja

GRAAFISEN KÄYTTÖLIITTYMÄN OHJELMOINTI JAVA SWING

Pakkauksen kokoaminen

JavaRMI 1 JAVA RMI. Rinnakkaisohjelmoinnin projekti 1 osa C Tekijät: Taru Itäpelto-Hu Jaakko Nissi Mikko Ikävalko

Olio-ohjelmointi Käyttöliittymä

5. HelloWorld-ohjelma 5.1

8. Näppäimistöltä lukeminen 8.1

Ohjelmointikielet ja -paradigmat 5op. Markus Norrena

Sisällys. 11. Rajapinnat. Johdanto. Johdanto

A) on käytännöllinen ohjelmointitekniikka. = laajennetaan aikaisemmin tehtyjä luokkia (uudelleenkäytettävyys)

14. Poikkeukset 14.1

Sisällys. 14. Poikkeukset. Johdanto. Johdanto

P e d a c o d e ohjelmointikoulutus verkossa

TIE Samuel Lahtinen. Lyhyt UML-opas. UML -pikaesittely

2. Olio-ohjelmoinista lyhyesti 2.1

Opintojakso TT00AA11 Ohjelmoinnin jatko (Java): 3 op Pakkaukset ja määreet

Luokat ja oliot. Ville Sundberg

7. Oliot ja viitteet 7.1

Kompositio. Mikä komposition on? Kompositio vs. yhteyssuhde Kompositio Javalla Konstruktorit set-ja get-metodit tostring-metodi Pääohjelma

Opintojakso TT00AA11 Ohjelmoinnin jatko (Java): 3 op. Poikkeukset ja tietovirrat: Virhetilanteiden ja syötevirtojen käsittely

812347A Olio-ohjelmointi, 2015 syksy 2. vsk. V Geneerisyys

TIE Ohjelmistojen suunnittelu

Rinnakkaisohjelmointi kurssi. Opintopiiri työskentelyn raportti

Aalto Yliopisto T Informaatioverkostot: Studio 1. Oliot ja luokat Javaohjelmoinnissa

812336A C++ -kielen perusteet,

Tehtävä 1. Tehtävä 2. Arvosteluperusteet Koherentti selitys Koherentti esimerkki

Luokkamalli LUOKKAKAAVIO. Tämän osan sisältö. Luokkamalli. Luokka ja olio. Luokkakaavio (class diagram)

Java kahdessa tunnissa. Jyry Suvilehto

Olio-ohjelmointi Johdanto suunnittelumalleihin. 1. Yleistä

Sisällys. 9. Periytyminen Javassa. Periytymismekanismi Java-kielessä. Periytymismekanismi Java-kielessä

on ohjelmoijan itse tekemä tietotyyppi, joka kuvaa käsitettä

Opintojakso TT00AA11 Ohjelmoinnin jatko (Java): 3 op

Pedacode Pikaopas. Java-kehitysympäristön pystyttäminen

Sisältö. Johdanto. Tiedostojen lukeminen. Tiedostojen kirjoittaminen. 6.2

YHTEYSSUHDE (assosiation)

JReleaser Yksikkötestaus ja JUnit. Mikko Mäkelä

Vertailulauseet. Ehtolausekkeet. Vertailulauseet. Vertailulauseet. if-lauseke. if-lauseke. Javan perusteet 2004

Yksikkötestaus. import org.junit.test; public class LaskinTest public void testlaskimenluonti() { Laskin laskin = new Laskin(); } }

Ohjelmoinnin perusteet Y Python

Operaattoreiden ylikuormitus. Operaattoreiden kuormitus. Operaattoreiden kuormitus. Operaattoreista. Kuormituksesta

Metodit. Metodien määrittely. Metodin parametrit ja paluuarvo. Metodien suorittaminen eli kutsuminen. Metodien kuormittaminen

Sisällys. 12. Näppäimistöltä lukeminen. Yleistä. Yleistä

Vesisika. metsiemme työmyyrä.

1. Omat operaatiot 1.1

Sisältö Johdanto. Tiedostojen lukeminen. Tiedostojen kirjoittaminen. 26.2

C++11 Syntaksi. Jari-Pekka Voutilainen Jari-Pekka Voutilainen: C++11 Syntaksi

Kertaus: yleistys-erikoistus ja perintä

14. Poikkeukset 14.1

Sisällys. 14. Poikkeukset. Johdanto. Johdanto

Sisällys. 9. Periytyminen Javassa. Periytymismekanismi Java-kielessä. Periytymismekanismi Java-kielessä

Javan perusteita. Janne Käki

19/20: Ikkuna olio-ohjelmoinnin maailmaan

Poikkeustenkäsittely

16. Javan omat luokat 16.1

Oliosuunnittelu. Oliosuunnittelu

Opintojakso TT00AA11 Ohjelmoinnin jatko (Java): 3 op. Tietorakenneluokkia 2: HashMap, TreeMap

Sisältö. Johdanto. Tiedostojen lukeminen. Tiedostojen kirjoittaminen. 6.2

Olio-ohjelmointi Syntaksikokoelma

Graafinen käyttöliittymä, osa 1

8. Näppäimistöltä lukeminen 8.1

Lohkot. if (ehto1) { if (ehto2) { lause 1;... lause n; } } else { lause 1;... lause m; } 16.3

Ohjelmoinnin peruskurssien laaja oppimäärä

Graafisen käyttöliittymän ohjelmointi Syksy 2013

5. HelloWorld-ohjelma 5.1

Yleistä. Nyt käsitellään vain taulukko (array), joka on saman tyyppisten muuttujien eli alkioiden (element) kokoelma.

Operaattorin ylikuormitus ja käyttäjän muunnokset

Listarakenne (ArrayList-luokka)

Transkriptio:

Olio-ohjelmointi Suunnittelumallit Adapter ja Composite Rakennemalleissa päähuomio kohdistetaan siihen, miten luokkia ja olioita yhdistellään muodostamaan laajempia rakenteita. Rakenteelliset luokkamallit käyttävät enimmäkseen periytymistä niiden luomiseksi, kun taas rakenteelliset oliomallit kuvaavat, miten saadaan aikaan uusia toimintoja olioita yhdistelemällä. Tässä osassa tutustutaan kahteen rakennemalliin (Adapter eli Sovitin ja Composite eli Rekursiokooste) suurimmaksi osaksi lähteen [Gam] pohjalta. 1. Adapter Olio-ohjelmoinnissa sattuu varsin usein, että käytettävissä on luokka, jonka toiminta vastaa ohjelman vaatimuksia, mutta sen rajapinta ei sovellu uuteen ympäristöön tai sen käyttö on hankalaa uudessa ympäristössä. Ongelma kohdataan usein pyrittäessä uudelleenkäyttöön. Luokka voi olla kirjastoluokka tai muuten sellainen, että sen rajapintaa ei voi muuttaa. Ongelma voidaan usein ratkaista kirjoittamalla sovitinluokka vanhan luokan ja uuden ohjelmakoodin väliin. Ratkaisu tunnetaan Adapter- eli Sovitin-suunnittelumallina. Javan luokkakirjastoissa mallia käytetään usein. Tutustutaan hieman graafisen käyttöliittymän ohjelmointiin käyttäen Swing-luokkia. Graafiseen käyttöliittymän perustuvan sovelluksen on aina perittävä ikkunaluokka JFrame, jotta voitaisiin toteuttaa sovelluksen pääikkuna. Usein laajennetaan suoraan luokkaa JFrame, mutta myös siitä perittyjä luokkia voi käyttää sovelluksen luokkana. Toteutus tapahtuu niin, että määritellään sovelluksen luokka JFramen aliluokaksi, sen muodostimessa tai muussa metodissa tehdään ikkunan alustustoimenpiteet. Yleensä mainmetodissa luodaan ikkuna ja näytetään se omassa säikeessään. Seuraavassa esimerkissä sovellus ainoastaan avaa uuden ikkunan, jonka otsikkona on Varsin mallikas ikkuna. Sovelluksen toteuttavan luokan nimeksi annetaan EkaIkkuna ja sen konstruktorissa annetaan sille otsikko ja asetetaan sen koko. Sovelluksen päämetodissa luodaan uusi olio luokasta ja näytetään se luotavassa uudessa säikeessä Frame-luokan setvisible() - metodilla. Huomaa, että jos tämä lause jätetään pois ohjelmasta, ikkuna kyllä luodaan, mutta se jää näkymättömiin. 1

Esimerkki. Ikkunan luova sovellus: import javax.swing.*; class EkaIkkuna extends JFrame { public EkaIkkuna() { // Annetaan ikkunalle otsikko this.settitle("varsin mallikas ikkuna."); // Määrätään ikkunan koko this.setsize(300,100); public void laitaesiin() { // Laitetaan näkyviin setvisible(true); public static void main(string[] args) { // Luodaan uusi EkaIkkuna -olio final EkaIkkuna akkuna = new EkaIkkuna(); ); // Näytetään ikkuna omassa säikeessään javax.swing.swingutilities.invokelater(new Runnable() { public void run() { akkuna.laitaesiin(); Ikkunan luomiseksi ei siis tarvita paljonkaan koodia, mutta edellistä sovellusta tutkittaessa huomataan, että mikäli ikkuna suljetaan normaaliin tapaan, jää itse sovellus vielä pyörimään ja se on lopetettava kirjoittamalta konsolilta Ctrl-c. Tämä vakavasti otettavissa sovelluksissa kiusallinen ominaisuus on JFrame-ikkunan sulkemisesta aiheutuva oletustapahtuma. Sovelluksen lopettaminen voidaan tehdä muuttamalla oletustapahtumaa tai, kuten seuraavassa, sopivan rajapinnan avulla. Kun painetaan hiirellä otsikkopalkin ristiä, aiheutuu ikkunatapahtuma, mutta sovellus ei reagoi siihen odotetusti. Javassa ikkunatapahtumia voidaan käsitellä sovelluksissa toteuttamalla AWTkirjastoon kuuluva WindowListener-rajapinta. Tämä rajapinta käsittää metodit 2

public void windowopened(windowevent e) Järjestelmä kutsuu tapahtuman kuuntelijan tätä metodia, kun ikkuna on avattu public void windowclosed(windowevent e) Järjestelmä kutsuu tapahtuman kuuntelijan tätä metodia, kun ikkuna on suljettu public void windowactivated(windowevent e) Järjestelmä kutsuu tapahtuman kuuntelijan tätä metodia, kun ikkuna on aktivoitu public void windowdeactivated(windowevent e) Järjestelmä kutsuu tapahtuman kuuntelijan tätä metodia, kun ikkuna menettää aktiivisuutensa public void windowiconified(windowevent e) Järjestelmä kutsuu tapahtuman kuuntelijan tätä metodia, kun ikkuna pienennetään public void windowdeiconified(windowevent e) Järjestelmä kutsuu tapahtuman kuuntelijan tätä metodia, kun ikkuna suurennetaan public void windowclosing(windowevent e) Järjestelmä kutsuu tapahtuman kuuntelijan tätä metodia, kun ikkuna suljetaan Sovelluksiin ei tarvitse aina liittää toimintoa jokaista ikkunatapahtumaa varten. Kun rajapinta toteutetaan, on kuitenkin sen jokainen metodi implementoitava. Tällaisessa tapauksessa riittää kun kirjoittaa tyhjän metodirungon niihin metodeihin, jotka vastaavat sovelluksen kannalta yhdentekeviä tapahtumia. Kun halutaan toteuttaa ainoastaan ikkunan sulkeminen, tarvitaan vain windowclosing()-metodiin koodia. Jotta voitaisiin käyttää rajapinnan metodeja, on koodissa toteutettava seuraavat asiat: 1) Luokan on toteutettava WindowListener rajapinta, mitä varten on myös importoitava java.awt.event-paketti. 2) On kirjoitettava kaikki rajapinnan metodit ja niistä windowclosing() metodiin lause System.exit(0); mikäli sovelluksen halutaan loppuvan ikkunan sulkemiseen. 3) Luotuun ikkunaan on lisättävä ikkunatapahtumien kuuntelija. Tämän voi tehdä kutsumalla ikkunan metodia addwindowlistener(). Usein kuuntelijaksi ilmoitetaan olio itse ja sen luokkaan kirjoitetaan tapahtumat käsittelevä koodi, mutta tapahtuman kuuntelijaolio voi olla jokin muukin. Kun tehdään nämä muutokset EkaIkkuna-luokan koodiin, saadaan seuraava sovellus: 3

import javax.swing.*; import java.awt.event.*; class TokaIkkuna extends JFrame implements WindowListener { public TokaIkkuna() { // Kuten edellisessä esimerkissä public void laitaesiin() { // Laitetaan näkyviin setvisible(true); // Lisätään ikkunaan kuuntelijaksi se itse! addwindowlistener(this); public static void main(string[] args) { // Luodaan uusi TokaIkkuna -olio final TokaIkkuna akkuna = new TokaIkkuna(); ); // Näytetään ikkuna omassa säikeessään javax.swing.swingutilities.invokelater(new Runnable() { public void run() { akkuna.laitaesiin(); /* Nämä metodit on rajapinnan takia implementoitava, mutta niiden rungot voidaan jättää tyhjiksi */ public void windowopened(windowevent e){ public void windowclosed(windowevent e) { public void windowactivated(windowevent e) { public void windowdeactivated(windowevent e) { public void windowiconified(windowevent e) { public void windowdeiconified(windowevent e) { // Vain tähän metodiin tarvitaan koodia! public void windowclosing(windowevent e) { System.exit(0); Nyt päästään Adapter-suunnittelumallin sovellukseen. Mikäli toteuttaa usein edellisen kaltaisia ikkunoita, tyhjien ikkunatapahtumametodien kirjoittaminen alkaa rasittaa mieltä. Siksi Javaan on toteutettu luokka WindowAdapter, joka toimii sovittimena WindowListener-rajapinnan ja ikkunaohjelman välillä. Sovittimeen on valmiiksi kirjoitettu kaikki rajapinnan vaatimat metodit tyhjinä. Näin ollen ohjelmoijan tarvitsee ainoastaan kirjoittaa luokka, joka perii luokan WindowAdapter ja uudelleenmääritellä haluamansa metodit. Tällöin pitää luonnollisesti lisätä ikkunan kuuntelijaksi uuden luokan olio. Esimerkiksi edellisen esimerkin ohjelmasta muodostuisi seuraavan kaltainen: 4

import javax.swing.*; import java.awt.event.*; class Poistu extends WindowAdapter { public void windowclosing(windowevent e){ System.exit(0); class TokaIkkunaAdapter extends JFrame { public TokaIkkunaAdapter() { // Kuten ennen public void laitaesiin() { // Laitetaan näkyviin setvisible(true); // Lisätään ikkunaan kuuntelijaksi Lopeta-olio addwindowlistener(new Poistu()); public static void main(string[] args) { // Luodaan uusi TokaIkkunaAdapter -olio final TokaIkkunaAdapter akkuna = new TokaIkkunaAdapter(); ); // Näytetään ikkuna omassa säikeessään javax.swing.swingutilities.invokelater(new Runnable() { public void run() { akkuna.laitaesiin(); Näin selvitään hieman pienemmällä vaivalla ja koodikin on vähän siistimpi. Kiinnostunut lukija löytää lisää tietoa Swing-komponenttien käytöstä lähteestä [Jav]. Adapter-mallin tarkoitus voidaan siis kuvata seuraavasti: Muuntaa luokan rajapinnan asiakkaiden vaatimaan muotoon ja mahdollistaa muuten rajapinnoiltaan yhteensopimattomien luokkien yhteistoiminnan. Adapteria voi soveltaa ainakin seuraaviin tapauksiin: Halutaan käyttää valmista luokkaa, jonka rajapinta ei vastaa tarpeita. Halutaan luoda uudelleenkäytettävä luokka, joka tulee toimimaan toistaiseksi tuntemattomien luokkien kanssa. On käytettävä useaa olemassa olevaa aliluokkaa, mutta ei ole käytännöllistä periä niitä kaikkia. Oliosovitin voi sovittaa yliluokkansa rajapinnan. 5

Adapter-mallista on kaksi muotoa: luokkasovitin ja oliosovitin. Luokkasovittimessa sovitettava luokka peritään sovitinluokkaan, kun taas oliosovittimessa sovitinluokka sisältää sovitettavan luokan olion osaolionaan. Alla on luokkasovittimen luokkakaavio: Kuva 1. Adapter-mallin luokkaversio. (Vrt. [Gam]) Huomaa, että malli käyttää moniperiytymistä. Tässä siis oletetaan, että on olemassa valmis luokka Adaptee, joka halutaan sovittaa vastaamaan asiakasluokan Request()-kutsuun. Adapteeluokassa onkin toiminnaltaan sopiva metodi, mutta se on nimeltään SpecificRequest(). Tässä tapauksessa Adaptee peritään luokkaan Adapter, johon kirjoitetaan metodi Request() kutsumaan Adaptee-luokan SpecificRequest-metodia. Olioversion luokkakaavio on seuraavan kaltainen: Kuva 2. Adapter-mallin olioversio. (Vrt. [Gam]) Yllä olevassa versiossa ei tarvitse käyttää moniperiytymistä, koska Adapteesta tulee nyt Adapterin osaolio. 6

Mallin osallistujat ovat siis Target Määrittelee sovelluskohtaisen rajapinnan, jonka kautta se voi kommunikoida Clientin kanssa. Client Kommunikoi olioiden kanssa Targetin määräämän rajapinnan kautta. Adaptee Määrittelee rajapinnan, joka on sovitettava uuteen ympäristöön Adapter Sovittaa Adapteen rajapinnan Targetin määräämään rajapintaan. Mallin version valintaan vaikuttaa, miten sovitinta käytetään. Oletetaan, että luokalla Adaptee on aliluokkia ja sen aliluokissa on määritelty operaatioita, jotka on myös sovitettava. Esimerkiksi yllä metodi SpecificRequest voisi olla uudelleenmääritelty Adaptee-luokan aliluokissa. Mikäli joissain tapauksissa olisi suoritettava aliluokan metodi, on käytettävä oliosovitinta. Perittäessähän luokka Adapter perisi ainoastaan Adaptee-luokan toteutuksen. Jos taas Adapterluokan on uudelleenmääriteltävä joitakin Adaptee-luokan toimintoja, on syytä käyttää luokkasovitinta. Aliluokassa uudelleenmäärittely on helpompi toteuttaa. Esitetään vielä esimerkki suunnittelumallin käytöstä. Oletetaan, että ohjelmassa on luokka StringContainer, joka sisältää jäsenmuuttujanaan String-taulukon. Syystä tai toisesta taulukon merkkijonoille on tehtävä jokin mutkikas operaatio ja palautettava muokattu taulukko. Luokan määrittely olisi class StringContainer { private: std::string* stringtable; int length; StringContainer(std::string* intable, int len); ~StringContainer(); int getlength() const; ; // Muokkaa stringtablen merkkijonot ja // palauttaa muokatun taulukon std::string* getmodifiedstrings() const; Tässä vaiheessa huomataankin, että on olemassa kirjastoluokka StringVectorModifier, jonka metodi modify tekee halutun operaation, mutta ei käytäkään taulukkoja vaan C++:n standardikirjaston kokoelmaluokkaa vector. class StringVectorModifier { void modify(std::vector<std::string> &strings); ; Näin ollen luokka StringContainer ei voi suoraan käyttää luokan metodia. Mikäli luokkaa StringContainer ei voida muuttaa, on kätevintä soveltaa Adapter-mallia. Nyt luokka StringContainer vastaa mallin kuvauksessa luokkaa Client ja luokka StringVectorModifier luokkaa Adaptee. Seuraavaksi tarvitaan luokkaa Target vastaava luokka; tämän on sisällettävä metodi, jota voidaan kutsua luokasta StringContainer: 7

class StringArrayModifier { virtual std::string* modifyarray(std::string *array, int length)=0; ; Luokan metodilla ei tarvitse olla lainkaan toteutusta, koska luokka peritään luokkaan, joka toteuttaa metodin. Siten metodi voidaan jättää edellisessä luokassa puhtaasti virtuaaliseksi. Tämän jälkeen voidaan kirjoittaa varsinainen sovitinluokka VectorArrayAdapter, joka siis vastaa mallin kuvauksessa luokkaa Adapter. Tehdään ensin mallista luokkaversio, jolloin VectorArrayAdapter perii sekä luokan StringVectorModifier että luokan StringArrayModifier: // Tiedosto VectorArrayAdapter.h class StringAdapter: private StringVectorModifier, public StringArrayModifier { std::string* modifyarray(std::string* array, int length); ; // Tiedosto VectorArrayAdapter.cpp std::string* VectorArrayAdapter::modifyArray(std::string* array, int length){ // Määrittele string-olioita sisältävä vector strings // Kirjoita taulukon array sisältämät merkkijonot // vectoriin strings // Muunna vectorin strings sisältö modify(strings); // Luo uusi taulukko modified_table, kirjoita // vectorin strings merkkijonot siihen // ja palauta taulukko return modified_table; Lopulta on enää lisättävä muunnoksen kutsuminen luokkaan StringContainer. Metodi getmodifiedstrings() voidaan toteuttaa nyt seuraavasti: std::string* StringContainer::getModifiedStrings() const { VectorArrayAdapter va; std::string *modifiedstrings = va.modifyarray(stringtable, length); return modifiedstrings; Käytännön sovelluksissa kannattaa huomata, että metodin palauttama taulukko on dynaamisesti luotu, joten sen tuhoaminen jää kutsujan vastuulle. 8

Edellisen ohjelman suunnittelumallin olioversio on muuten samanlainen kuin edellinen, mutta nyt VectorArrayAdapter ei peri luokkaa StringVectorModifier, vaan sisältää StringVectorModifier-olion osanaan. Muutoksia tulee siis ainoastaan luokkaan VectorArrayAdapter: // Tiedosto VectorArrayAdapter.h class VectorArrayAdapter: public StringArrayModifier { private: // Vector modifier as an attribute StringVectorModifier svmmodifier; std::string* modifyarray(std::string* array, int length); ; // Tiedosto VectorArrayAdapter.cpp std::string* VectorArrayAdapter::modifyArray(std::string* array, int length){ // Määrittele string-olioita sisältävä vector strings // Kirjoita taulukon array sisältämät merkkijonot // vectoriin strings // Muunna vectorin strings sisältö svmmodifier.modify(strings); // Luo uusi taulukko modified_table, kirjoita // vectorin strings merkkijonot siihen // ja palauta taulukko return modified_table; 9

2. Composite Composite- eli Rekursiokooste-mallin juuret ovat graafisten käyttöliittymäliittymäkomponenttien rakentamisessa. Komponenttien muodostamaa koostetta voidaan käyttöliittymässä käsitellä yksittäisenä komponenttina. Suunnittelumalli syntyy, kun yleistetään tilannetta: muissakin yhteyksissä tarvitaan olioiden ryhmiä, joita halutaan käsitellä kuin yksittäistä oliota. Mallin avulla voidaan luoda luokkahierarkia, jonka eräät luokat määrittelevät alkeisolioita (kuten primitiiviset käyttöliittymäkomponentit) ja toiset luokat määrittelevät yhdisteolioita, jotka koostavat alkeisolioita monimutkaisemmiksi yhdistelmiksi. Rekursiokooste-mallia käsitellään myös kurssissa Oliosuuntautunut analyysi ja suunnittelu. Suunnittelumallin tarkoitus on järjestää oliot puuhierarkioiksi, jotka kuvaavat osa-kokonaisuussuhteita. Malli tekee mahdolliseksi osien ja kokonaisuuksien yhdenmukaisen käsittelyn. Mallin luokkarakenne on kuvattu alla Kuva 3. Composite-mallin luokkakaavio. (Vrt. [Gam]) Component on sekä primitiivisten olioiden (Leaf) että yhdistelmäolioiden (Composite) kantaluokka. Kaaviosta käy ilmi, että Composite-luokka voi sisältää Component-olioita, jotka voivat nyt olla joko Leaf-luokan tai edelleen Composite-luokan olioita. 10

Tyypillinen mallia käyttävä oliorakenne voisi olla Kuva 4. Composite-mallin tyypillinen oliokaavio. Mallin toimijat ovat siis Component, joka määrittelee käytettävien olioiden rajapinnan. Toteuttaa oletustoiminnot kaikille luokille. Määrittelee rajapinnan lapsiluokkiensa käsittelyyn. Leaf, joka esittää primitiivisiä koosteen olioita. Leaf-olioilla ei ole lapsia. Composite, joka määrittelee toiminnot niille olioille, joilla voi olla lapsia. Toimii säiliönä lapsikomponenteille. Toteuttaa lapsien käsittelyyn liittyvät Componetrajapinnan operaatiot. Client, joka käsittelee koosteen olioita Component-rajapinnan kautta. Suunnittelumallilla on useita etuja. Sen avulla asiakasluokan koodi voidaan pitää yksinkertaisena, koska sekä yhdistelmäolioita että primitiivisiä olioita voidaan käsitellä yhdenmukaisesti. Asiakasolion ei yleensä tarvitse tietää, käsitteleekö se komponenttia vai primitiivistä oliota. Lisäksi on helppoa lisätä uusia komponentteja ohjelmaan, sillä uusien aliluokkien oliot toimivat automaattisesti entisen ohjelmakoodin kanssa. Siten asiakasolion koodia ei tarvitse muuttaa lisättäessä uusia komponenttiluokkia. Toisaalta mallin yleisyys voi aiheuttaa vaikeuksia. Joskus pitäisi rajoittaa komponentin sisältämien olioiden tyyppiä; tämän voi käytännössä toteuttaa vain ajonaikaisilla tyypintarkistuksilla. 11

Tarkastellaan esimerkkiä: Toteutetaan laitevarasto Composite-suunnittelumallin avulla. Varastossa voi olla erilaisia laitteita, joihin voi kuulua erilaisia osakomponentteja. Luokan kantaluokaksi laaditaan mallin mukaan seuraava abstrakti luokka: class EquipmentComponent { virtual void operation()=0; virtual void add(equipmentcomponent *component)=0; virtual void remove(equipmentcomponent *component)=0; virtual EquipmentComponent* getchild(int index)=0; virtual ~EquipmentComponent(){ ; Kaikki varastossa olevat laitteet perivät tämän luokan, myös varasto-olio itse. Huomaa, että luokan aliluokissa metodeista tulee dynaamisesti sidottavia virtual-määreen takia. Toteutetaan tässä varastoluokka, kameraluokka ja objektiiviluokka. Kameraan voi liittyä yksi tai useampia objektiiveja, objektiivi voi olla myös itsenäisenä varastossa. Objektiivit eivät voi sisältää komponentteja, joten ne ovat primitiivisiä olioita: niiden luokka vastaa mallin kuvauksen Leafluokkaa. Varastoluokka ja kameraluokka ovat Composite-tyyppisiä luokkia. Toteutetaan aluksi objektiivia kuvaava luokka: // Tiedosto Objective.h class Objective: public EquipmentComponent { private: std::string name; Objective(std::string s); ~Objective(){ void operation(); void add(equipmentcomponent *component); void remove(equipmentcomponent *component); EquipmentComponent* getchild(int index); ; // Tiedosto Objective.cpp Objective::Objective(std::string s): name(s) { void Objective::operation() { std::cout << "Objective: " << name << std::endl; void Objective::add(EquipmentComponent *component) { throw std::runtime_error("no adding in leaf"); void Objective::remove(EquipmentComponent *component){ throw std::runtime_error("no removing from leaf"); EquipmentComponent* Objective::getChild(int index) { throw std::runtime_error("no components in leaf"); 12

Yllä, samoin kuin jatkossa muussakin koodissa on jätetty mainitsematta käytetyt includetiedostot. Tässä tapauksessa oliolle suoritettava operaatio vain tulostaa sen nimen. Primitiivisellä oliolla ei voi olla lapsia, joten niiden käsittelymetodit heittävät poikkeuksen. Seuraavaksi kirjoitetaan kameraa kuvaava luokka, joka on Composite-tyyppinen. Siten siihen on lisättävä lapsien käsittelymetodit. // Tiedosto Camera.h class Camera : public EquipmentComponent { typedef std::vector<equipmentcomponent*> ComponentVector; private: std::string name; ComponentVector children; Camera(std::string s); ~Camera(){; ; void operation(); void add(equipmentcomponent *component); void remove(equipmentcomponent *component); EquipmentComponent* getchild(int index); // Tiedosto Camera.cpp Camera::Camera(std::string s):name(s) { void Camera::operation() { std::cout << "Camera " << name << " with objectives:" << std::endl; ComponentVector::iterator iter; for(iter = children.begin(); iter!= children.end(); iter++) { (*iter)->operation(); std::cout <<"------------------------------------------"<<std::endl; void Camera::add(EquipmentComponent *component) { children.push_back(component); void Camera::remove(EquipmentComponent *component){ ComponentVector::iterator iter; iter = std::find(children.begin(),children.end(),component); if(iter!= children.end()){ children.erase(iter); EquipmentComponent* Camera::getChild(int index) { return children.at(index); 13

Komponentin lapset sijoitetaan vector-tyyppiseen kokoelmaan. Tällä kertaa operaationa on tulostaa olion nimi ja tehdä kaikille lapsiolioille niiden operaatio. Lisäämiseen ja poistamiseen käytetään vector-luokan metodeja, vectorin selaajaa ja STL-kirjaston algoritmia find. Laaditaan vielä koko varastoa kuvaava luokka (joka voisi myös olla esimerkiksi varaston kameraosasto ja voitaisiin liittää yleisempään laitevarastoon): // Tiedosto EquipmentStore.h class EquipmentStore : public EquipmentComponent { typedef std::vector<equipmentcomponent*> ComponentVector; private: ComponentVector children; EquipmentStore() { ~EquipmentStore(){ void operation(); void add(equipmentcomponent *component); void remove(equipmentcomponent *component); EquipmentComponent* getchild(int index); ; // Tiedosto EquipmentStore.cpp // Muut metodit kuten luokassa Camera void EquipmentStore::operation(){ std::cout << "Store contains following items:" << std::endl; std::cout << "*******************************************" << std::endl; ComponentVector::iterator iter; for(iter = children.begin(); iter!= children.end(); iter++) { (*iter)->operation(); std::cout << "*******************************************" << std::endl; Koodi on operation()-metodia lukuun ottamatta samanlainen kuin luokassa Camera. Lopuksi voitaisiin kirjoittaa luokkia käyttävä asiakasohjelma esimerkiksi seuraavasti: 14

int main() { EquipmentComponent *objective1, *objective2, *objective3, *objective4; EquipmentComponent *camera1, *camera2, *camera3; EquipmentComponent *store = new EquipmentStore(); objective1 = new Objective("Tamron 50-150"); objective2 = new Objective("Nikon 50"); objective3 = new Objective("Canon 35-150"); objective4 = new Objective("Leica 350"); camera1 = new Camera("Canon 1000"); camera2 = new Camera("Leica"); camera3 = new Camera("Nikon pocket"); store->add(camera1); store->add(camera2); store->add(camera3); store->add(objective2); camera1->add(objective1); camera1->add(objective3); camera2->add(objective4); store->operation(); camera1->remove(objective1); store->remove(camera2); store->operation(); // Tuhotaan kekodynaamiset oliot return 0; Tässä luodaan pieni kameravarasto, tulostetaan se, poistetaan yksi kamera ja yksi objektiivi. Lopuksi tulostetaan muokattu varasto. Jos edellisessä ohjelmassa yritettäisiin poistaa komponenttia objektiivista, ajauduttaisiin poikkeustilanteeseen. Koodissa näkyy kuitenkin heikkous, joka aiheutuu mallin yleisyydestä: Kameraolion komponentiksi voitaisiin vaikeuksitta lisätä varasto-olio, vaikka loogisesti se ei ole järkevää. Näin ollen ohjelmassa pitäisi rajoittaa kameran sisältämien komponenttien tyyppi objektiiviksi. Tehdään vielä lopuksi joitakin huomioita Composite-mallin toteutuksesta. Joskus on käytännöllistä pitää lapsikomponenteissa yllä viitettä niiden vanhempaan, toisin sanoen komponenttiin, joka sisältää ko. lapsikomponentin. Tällöin on kiinnitettävä huomiota siihen, että viitteet pysyvät kunnossa, kun rakenteeseen lisätään tai siitä poistetaan komponentteja. Edelleen komponenttien jakaminen voi hankaloitua, mikäli komponentilla voi olla vain yksi vanhempi. Sekä primitiiviset oliot (mallissa luokka Leaf) että alikomponentteja sisältävät komponentit (mallissa luokka Composite) perivät luokan Component. Jotta kummankin tyyppisiä komponentteja voitaisiin käsitellä mahdollisimman yhtenäisesti, tulisi Component-luokan esitellä mahdollisimman monta näiden luokkien yhteistä operaatiota. Tämä kuitenkin rikkoo tietyssä mielessä olio-ohjelmoinnin periaatetta, jonka mukaan kantaluokkaan tulisi määritellä ainoastaan kaikille aliluokille käyttökelpoisia metodeja. Monet operaatiot ovat hyödyllisiä Compositeluokissa, mutta hyödyttömiä Leaf-luokissa, esimerkiksi lapsiin liittyvät operaatiot. Yksi tapa ratkaista asia on kirjoittaa Component-luokkaan sellaiset oletustoteutukset, jotka soveltuvat Leaf- 15

luokille. Composite-aliluokat sen sijaan määrittelevät ne tarvittaessa uudelleen. Aiemmassa esimerkissä kantaluokan metodit on jätetty tyhjiksi ja siirretty poikkeuksen aiheuttaminen Objective-luokan metodeihin. Jos haluaa korostaa turvallisuutta käsittelyn yhtenäisyyden kustannuksella, voi siirtää lapsien käsittelymetodit kokonaan Composite-luokkiin. Komponenttien muodostamasta rakenteesta poistetaan olioita dynaamisesti. Oliot on tällöin tuhottava, etteivät ne jää kuormittamaan muistia. Kielissä, joissa ei ole toteutettu roskien keruuta (esimerkiksi C++), on jollekin toimijalle annettava vastuu muistin vapauttamisesta. Yleensä ohjelma suunnitellaan niin, että komponentti tuhoutuessaan tuhoaa myös lapsensa. Edellä Composite-tyyppiset oliot (Camera ja EquipmentStore) on kuitenkin yksinkertaisuuden vuoksi toteutettu siten, että ne eivät ole vastuussa lapsiensa tuhoamisesta, vaan oletetaan, että oliot tuhotaan muualla. Mainittakoon, että Java-kielellä toteutettaessa ominaisuudesta ei syntyisi ongelmaa, koska sen automaattinen roskien keruu vapauttaa oliot, joihin ei enää ole viitteitä. Lähteet [Gam] Gamma, Helm, Johnson, Vlissides: Design Patterns: Elements of Reusable Object- Oriented Software, Addison-Wesley 1995 [Jav] Java tutorials: Creating GUI with Swing (URL: http://download.oracle.com/javase/tutorial/uiswing/index.html, viitattu 9.10.2013) 16