Flere design mønstre 19. september 2002, Tore Berg Hansen, TISIP Kursleksjonene er forfatters eiendom. Som kursdeltaker kan du fritt bruke leksjonene til eget personlig bruk. Kursdeltakere som ønsker å bruke leksjonene til undervisning eller kursformål må ta kontakt med TISIP for nærmere avtale. Tore Berg Hansen
Sammendrag Denne leksjonen tar for seg flere design mønstre. Design behandles dermed i to leksjoner. Det gjør vi fordi vellykket design er av avgjørende betydning for et prosjekts suksess eller fiasko. En tråd gjennom disse to leksjonene om design er at design kan gjøres systematisk og metodisk gjennom bruk av sunne prinsipper og god praksis. Prinsipper og praksis er nedfelt i design mønstre (patterns). Design mønstre beskriver enkle og elegante løsninger for spesifikke problemer i objektorientert design. Å lage en god objektorientert design er en utfordrende oppgave. Å gjøre en design gjenbrukbar er en enda større oppgave. Bruk av design mønstre hjelper oss med dette. Nå man finner en god løsning vil man bruke den om og om igjen. Det er noe av hensikten med design mønstre.
Innhold Introduksjon...1 Prinsipper for god design...1 Polymorfisme...3 Mellomledd (indirection)...4 Ren fabrikasjon...5 Beskyttelse mot variasjoner...5 Factory...6 Singleton...7 Fasade...7 Observer...7 Lesestoff...7 Referanser...7
Introduksjon En av nøklene til et vellykket utviklingsprosjekt er god design. Det viktigste er designerens dyktighet. Men dyktige folk oppnår resultater gjennom bruk av sunne prinsipper og praksis. Disse er bl.a nedfelt i design mønstre. I forrige leksjon så vi på fem sentrale mønstre. Disse fem er de som trolig anvendes hyppigst fordi de gir svar på de mest vanlige design problemer. Disse mønstrene tilhører de såkalte GRASP 1 mønstre lansert av lærebokforfatteren [1]. De fem GRASP mønstrene var: Informasjonsekspert Skaper (Creator) Høy kohesjon Løs kobling Kontroller I denne leksjonen skal vi se på noen flere GRASP mønstre og noen fra The Gang of Four (GoF) [2]. GoF mønstre skriver seg fra noen av pionerene innen design mønstre. De var fire personer og er kjent gjennom utgivelsen av en bok som har blitt et av standardverkene innen for dette fagfeltet. De mønstrene vi skal se på i denne leksjonen er: Polymorfisme (GRASP) Mellomledd (GRASP) Ren fabrikasjon (GRASP) Beskyttelse mot variasjon (GRASP) Factory (GoF) Singleton (GoF) Fasade (GoF) Observer/Publish-Subscribe/Delegation Event Model (GoF) Men før vi gjør det skal vi ta for oss noen generelle retningslinjer for god design. Prinsipper for god design Vi skal her kort ta for oss noen generelle prinsipper og retningslinjer for god design. Stoffet er i hovedsak hentet fra [2]. Et hovedpoeng er at programvare må designes for gjenbruk. Det stiller spesielle krav til designeren. De retningslinjer som presenters her skal gjøre det lettere å foreta de nødvendige avveininger. Disse retningslinjene finner man også igjen i de forskjellige mønstre. Før vi går løs på retningslinjene må vi avklare noen begreper. Ojektorienterte programmer settes sammen av objekter. Et objekt inneholder både data og prosedyrer som operer på disse dataene. Slike prosedyrer kalles typisk metoder eller operasjoner avhengig av modellerings- eller programmeringsspråk. Et objekt utfører en operasjon som svar på en forespørsel ( også kalt en melding). En operasjon har en signatur. Den utgjøres av operasjonens navn, parametere og returverdi. Ikke alle operasjoner har parametere eller returverdi. 1 GRASP General Responsibility Assignment Software Patterns 1
En samling (sett, mengde) signaturer for et objekt kalles objektets grensesnitt (interface). En type brukes for å benevne et bestemt grensesnitt. Et objekt er av type Rektangel hvis det godtar alle forespørsler om operasjoner som er definert i grensesnittet med navn Rektangel. Det betyr også at et objekt kan ha flere typer og forskjellige objekter kan ha typer som er felles. Eller med andre ord så kan objekter ha flere grensesnitt av forskjellig type. Grensesnitt er fundamentale i objektorienterte systemer. Dette fordi objekter kun er kjent gjennom sine grensesnitt. En klasse inneholder et objekts definisjon. Klassen spesifiserer objekters interne data og operasjonene som kan utføres på dataene. Instansiering er begrepet som brukes om det å skape objekter. Objekter som gjør jobben i et program må skapes. Det som i praksis skjer er at det avsettes plass i datamaskinens minne for objektets interne data såkalte instansvariabler og deres tilhørende operasjoner. Mange lignende objekter kan skapes (instansieres) fra samme klasse. Abstrakte klasser er klasser hvis eneste hensikt er å definere et felles grensesnitt for nedarvede klasser. Javas Interface er et eksempel på denne idéen. C++ klasser med rent virtuelle funksjoner er et annet eksempel. Vi vil i dette notatet bruke begrepet Interface som synonym for en abstrakt klasse. I [2] legges det vekt på at det er to typer arv. Den ene er arv mellom klasser. Det andre er arv mellom Interface. Arv mellom klasser betyr at definisjonen av et objekts implementasjon delvis består av et annet objekts implementasjon. Dvs at man deler på kode og representasjon. Arv av Interface forteller at et objekt kan brukes i stedet for et annet. Java har denne mekanismen, mens C++ ikke har den. Man kan få til det samme i C++ ved å lage abstrakte klasser som kun inneholder rent virtuelle funksjoner. Med bakgrunn i disse begrepsavklaringene skal vi lansere noen grunnleggende design prinsipper. Disse er: Programmer for et grensesnitt, ikke en implentasjon. Grunnen er at polymorfisme er avhengig av det. Arv må brukes med forsiktighet. La klasser arve fra abstrakte klasser. Da vil alle subklasser få det samme grensesnitt. Det er to fordeler med dette. 1. En klient behøver ikke bekymre seg for typen til objekter så lenge de har det grensesnitt som klienten har bruk for. 2. En klient behøver ikke kjenne til klassen som implementerer objektet. Foretrekk komponering med objekter fremfor arv mellom objekter. Dette dreier seg om gjenbruk. Ved komponering menes at man får ny og mer avansert funksjonalitet ved å sette sammen objekter. Motstykket er gjenbruk ved arv, hvor man lager nye objekter ved å utvide funksjonaliteten til eksisterende objekter. Det er det man vanligvis forstår med arv. Ulempen er at man bryter med prinsippet om løs kobling fordi forelderklassens indre ofte må være synlig for subklassen. På den måten blir subklassen avhengig av endringer i forelderklassens definisjon. Og man kan bli tvunget til å gjøre endringer i en forelderklasse for at en subklasse skal fungere hensiktsmessig i gitte situasjoner. Det kan igjen føre til at andre subklasser må endres. Man snakker her om hvit-boks gjenbruk. Motstykket er svart-boks gjenbruk. Her skjer gjenbruk ved bruk av objekter med et definert grensesnitt settes sammen. Det trengs ingen kunnskap om de indre detaljer. Slik komposisjon kan gjøres i kjøretid. 2
Arv er bestemt ved kompilering. Moderne komponentteknologi baserer seg på komposisjon. Det skal vi komme tilbake til i neste leksjon. Ved å fokusere på komposisjon blir klassehierarkiene små og vokser ikke til store uhåndterlige monstre. Men nå er det ikke slik at det alltid finnes egnede objekter å komponere med. Arv gjør det enkelt å lage nye komponenter, som man så kan komponere med. Så det er ikke et enten eller, men en balansert avveining. Men det understrekes at man må unngå den ukritiske bruk av arv som går igjen i mange lærebøker i objektorientert programmering. Vi avslutter dette kapitlet med eksempel på komposisjon som kalles delegering. Se figuren. Vindu Rektangel - bredde - hoyde { double Vindu::areal() { return rektangel.areal(); } } { double Rektangel::areal() { return bredde * hoyde; } } Det dette klassediagrammet forteller er at Vindu delegerer til Rektangel å beregne sitt areal. Vindu har altså en referanse til rektangel. En alternativ løsning ville være å la Vindu arve fra Rektangel fordi vinduer ofte er rektangulære. Men det vil være en dårligere løsning fordi da binder man et vindu til rektangel under kompilering. Nå finnes det vinduer som har andre former sirkulære, elliptiske, triangulære. Med delegering og polymorfisme (neste kapittel), kan man vente til kjøretid og dynamisk å bestemme hvilken geometrisk form et vindu skal assosieres med. Beregning av areal går bra så lenge grensesnittet er det samme. Polymorfisme Polymorfisme er et sentralt begrep innenfor objektorientering og kan ha forskjellig betydning avhengig av sammenhengen begrepet brukes i. Polymorf betyr mangeformet, noe som opptrer i mange former. I objektorientert sammenheng betyr det at vi gir samme navn til likeartet oppførsel hvor implementasjonen er forskjellig. Det er slik begrepet brukes i forbindelse med mønstre. Mønsteret er beskrevet i 3
kapittel 22 fra side 326 i læreboken. Vi skal ta et eksempel. Det er behov for å beregne areal og omkrets av forskjellige geometriske figurer. Figurene kan være sirkel, trekant, firkant, ellipse osv. Det er samme operasjon som skal utføres, men på forskjellige måter. Derfor kan vi gi operasjonene samme navn slik som vist i denne figuren. <<Interface>> IGeomFigur + omkrets() : double Sirkel Trekant Firkant Ellipse + omkrets() : double + omkrets() : double + omkrets() : double + omkrets() : double Her har vi et pluggbart opplegg ved at man kan plugge inn nye figurer etter behov. I et programmeringsspråk som Java kan deler av koden som implementerer dette se ut som følger. Igeomfigur enfigur; Sirkel ensirkel = new Sirkel(); Trekant entrekant = new Trekant(); Firkant enfirkant = new Firkant(); Ellipse enellipse = new Ellipse(); enfigur = enellipse; arealet = enfigur.areal(); omkretsen = enfigur.omkrets(); I de to siste linjene kan man si at beregningene blir utført for en eller annen figur. Forhistorien bestemmer hvilken figur og dermed hvordan beregningene blir utført. I dette tilfellet kan det se ut som beregningene utføres for en ellipse. Mellomledd (indirection) Mønsteret er beskrevet fra side 332 i læreboken. Poenget er å gi løsere kobling slik at potensialet for gjenbruk øker. Eksemplet i boken viser en situasjon hvor det er behov 4
for å kommunisere over et nettverk for å hente informasjon. For at et domeneobjekt 2 ikke skal være direkte avhengig av en gitt kommunikasjonsprotokoll, legger man inn et mellomledd. Dette kan byttes hvis/når man har behov for andre protokoller. Mekanismen for å gjøre bytte kan være basert på polymorfisme. Det vil si at domeneobjektet alltid ser det samme grensesnittet og er uavhengig av hvordan grensesnittet implementeres i en gitt sammenheng. Ren fabrikasjon Pure fabrication heter detter mønsteret på engelsk. Beskrivelsen starter på side 329 i læreboken. Her er poenget å løse opp en design som gjennom bruk av andre mønstre, kanskje spesielt Ekspert, har ført til lav kohesjon og tett kobling. Bruk av Ekspert fører gjerne til at objekter som er hentet fra problemdomenet får for mye å gjøre. En løsning på dette representerer altså mønsteret Ren fabrikasjon. Det sier at man skal legge ansvar som hører sammen (høy kohesjon) til et kunstig objekt som man fabrikkerer for formålet. Ettersom høy kohesjon og løs kobling på en måte er to sider av samme sak, resulterer bruk av mønsteret også gjerne i løsere kobling og økt mulighet for gjenbruk. Et slikt kunstig objekt vil ikke ha noe motstykke i problemdomenet. Mange av objektene som er Mellomledd (mønsteret som er beskrevet foran) er gjerne slike rene fabrikasjoner. Beskyttelse mot variasjoner På originalspråket heter dette mønsteret Protected Variations (side 334 i læreboken). Det er mer et design prinsipp enn et mønster. Poenget er at man skal forsøke så langt det er praktisk og økonomisk mulig å identifisere steder i programvaren hvor det er stor sannsynlighet for at noe vil endres. For å beskytte seg mot slike endringer søker man å legge grensesnitt rundt slike steder slik at innvirkningen av mulige endringer blir minimal for andre deler av programsystemet. De to foregående mønstre kan være eksempler på dette prinsippet. Også de generelle objektorienterte prinsipp om abstraksjon og skjuling av informasjon går inn under prinsippet om Protected Variations. I sin første utgave av læreboken hadde Larman et mønster han kalte Ikke snakk til fremmede. Poenget er at et objekt må beskyttes fra kunnskap om fremmede objekter. Med fremmede objekter menes objekter som ligger langt ute i en kjede av objekter. Objekter som er direkte koblet tilhører den nære familie, som man trygt kan henvende seg til. Her gjengis eksemplet fra forrige utgave av læreboken. Det baserer seg på sammen case som i denne utgaven av læreboken. Kasseterminal + beløptilbetaling() : double Salg + betaling() : Betaling Betaling + forfaltbeløp() : double Denne figuren viser hvilke objekter som er involvert når det skal tas betaling. Betalingen skjer i Kasseterminalen. Denne henvender seg så til Salg som kan 2 Med domeneobjekt mener vi her et objekt som har ansvar for å utføre noe av den egenlige nytteverdi i programmet, som f.eks en beregning. Et slikt objekt skal kunne gjenbrukes i forskjellige sammenhenger og må derfor ikke være sterkt koblet til bestemte andre objekter. 5
returnere en referanse til Betaling som kjenner og kan returnere det forfalte beløp. Samarbeidsdiagrammet viser hendelsesforløpet. : Kasseterminal 1: betaling( ) : Salg 2: forfaltbeløp( ) : Betaling Denne figuren viser klart at her brytes prinsippet om at man ikke skal snakke til fremmede fordi Betaling er fremmed for Kasseterminal. De neste figurene viser en bedre løsning i henhold til prinsippet om ikke å snakke til fremmede. Kasseterminal + beløptilbetaling() : double Salg + beløptilbetaling() : double Betaling + forfaltbeløp() : double Det som er gjort her er at i stedet for at Salg returnerer en referanse til Betaling returneres i stedet det forfalt beløp. Samarbeidsdiagrammet ser nå slik ut. 1: beløptilbetaling( ) : Kasseterminal : Salg 2: forfaltbeløp( ) : Betaling Her er kasseterminal frikoblet fra interne detaljer i Betaling. Jobben med å få fatt i det forfalte beløp er overlatt til det nærmeste objektet som Kasseterminal ser direkte. Factory Beskrivelsen starter på side 346 i læreboken. Jeg velger å bruke den engelske betegnelsen. Det dreier seg om hvordan man skal håndtere kreering av objekter når 6
man må ta helt spesielle hensyn. Slike spesielle hensyn kan være at logikken rundt kreeringen kan være spesiell. Uten å gå i detalj så brukes denne mekanismen i Microsoft sin komponentmodell, COM. Factory kommer også til anvendelse når man bruker prinsippet om at man fortrinnsvis skal programmere for et grensesnitt og ikke implementasjon. I slike tilfeller lar man et objekt, som gjerne er en Singleton, ta seg av kreering av objekter. En slik objektfabrikk returnerer så en referanse til et objekt som har det ønskede grensesnitt. Eventuelle endringer blir dermed isolert til et sted. Singleton Dette mønsteret brukes når man har behov for bare en forekomst (instance) av en klasse. Se side 348 i læreboken. Løsningen er å definere en statisk metode i den klassen som skal returnere forekomsten (the Singleton). Fasade Side 368 i læreboken. Dette mønsteret skal løse problemet med samspill med andre systemer eller subsystemer. Et Fasadeobjekt representerer et kontaktpunkt i slike situasjoner. Bak fasaden kan det skjule seg mange objekter. Det behøver ikke engang være objekter, men systemer eller subsystemer realisert i andre språk. Hvis det dreier seg om systemer som en bedrift har investert mange penger i tidligere og som man gjerne vil forsette å bruke sier man at systemet tilhører arvegodset (legacy system på engelsk). Man kobler altså det eksisterende system til det nye via et fasadeobjekt. I denne sammenheng betegnes fasadeobjektet et wrapperobjekt. Observer Dette mønsterert går også under navnene Publish-Subscibe/Delegation Event Model. Se side 372. Mønsteret har spesiell anvendelse i hendelsesorienterte programvaresystemer. Prinsippet anvendes på problemer hvor man ønsker å unngå for tett kobling mellom objekter som utløser en hendelse og det objektet som skal reagere på hendelsen. Et eksempel på en slik situasjon er kommuniksjon mellom et grafisk brukergrensesnitt og domeneobjekter. For dere som er familiær med Java så har dere støtt borti løsninger etter dette mønsteret i form av forskjellige lyttere (ActionListener, WindowListener er eksempler). Lesestoff Mønstrene som er behandlet er beskrevet i kapittel 22 og 23 i læreboken. Referanser 1. Craig Larman, Applying UML and Patterns. An Introduction to Object- Oriented Analysis and Design and the Unified Process, Prentice Hall, andre utgave 2002, ISBN 0-13-092569-1. 2. Erich Gamma & al, Design Patterns. Elements of Reusable Object-Oriented Software, Addison-Wesley, 1995, ISBN 0-201-63361-2. 7