Gert Florijn1.,2, Mark van Elswijk2
1 Universiteit Utrecht, Vakgroep Informatica
2Software Engineering Research Centre, SERC
E-mail: florijn@serc.nl, elswijk@serc.nl
(Dit artikel is oorspronkelijk verschenen in Informatie jaargang 38, nr. 2, februari
1996 onder de titel: OO ontwerptrend: Design Patterns.
Copyright © Kluwer bedrijfsinformatie, 1996)
Het maken van goede object-georiënteerde (OO) modellen is niet eenvoudig. De kwaliteit
en ervaring van individuele ontwikkelaars zijn nog steeds erg belangrijk. Daarom is het
interessant om te zien hoe we de kennis en ervaring van goede OO ontwerpers toegankelijk
kunnen maken. In de OO wereld wordt hiervoor toenemend gebruik gemaakt van patterns.
Een pattern is een beschrijving (in een standaard formaat) van een algemene oplossing voor
een veel voorkomend OO ontwerpprobleem. Patterns zijn (meestal) domein-, probleem- en
programmeertaal onafhankelijk. Bij het gebruik van een pattern moet de ontwerper de
algemene oplossing vertalen naar de context van het specifieke probleem en de gehanteerde
implementatieomgeving. Overwegingen hieromtrent maken vaak deel uit van een
patternbeschrijving.
Een pattern omvat veelal meerdere klassen (of rollen), geeft aan welke diensten (methoden)
die klassen moeten bieden en hoe de objecten en klassen samenhangen en samenwerken.
Patterns bieden dan ook - naast hergebruik van ervaring - interessante mogelijkheden om te
denken in en te werken met grotere eenheden dan individuele objecten of klassen.
In dit artikel geven we een overzicht van de achtergronden en de stand van zaken rond het
gebruik patterns binnen OO systeemontwikkeling.
Bij het ontwikkelen van nieuwe computertoepassingen beginnen we tegenwoordig zelden of nooit vanaf nul. Via software bibliotheken, standaard pakketten, generatoren en besturingssystemen herbruiken we standaard oplossingen voor bepaalde taken. Maar dit is niet de enige vorm van hergebruik. Ontwikkelaars die een systeem bouwen maken ook gebruik van kennis en ervaring die ze hebben opgedaan bij het ontwikkelen van eerdere systemen. Men weet, bijvoorbeeld, dat een standaard bibliotheek een fout bevat waar op een bepaalde manier "omheen" geprogrammeerd moet worden, of dat een bepaalde constructie in een programmeertaal minder efficiënt is dan een andere. Op dezelfde manier weet men dat een bepaalde opsplitsing in componenten een goede en efficiënte architectuur biedt voor een bepaald soort toepassing, of dat bepaalde constructies een datamodel flexibeler maken.
Dergelijke inzichten worden in de loop der tijd opgebouwd; door "schade en schande" heeft men geleerd dat een bepaalde oplossing voor een bepaald soort probleem wel of niet werkt en welke overwegingen daarbij een rol spelen. Dit soort ervaring gaat vaak over de grenzen van toepassingsdomeinen, implementatieplatforms en programmeertalen heen, en is mede daardoor niet te verpakken in "standaard" programmatuur. Maar het heeft wel grote invloed op de snelheid en effectiviteit waarmee een nieuw systeem ontworpen en gebouwd wordt. Men herkent problemen eerder, komt sneller tot een keuze voor een oplossing en ontwijkt de valkuilen die eerder zijn ontdekt.
Om meer te kunnen profiteren van de ervaring van (goede) ontwikkelaars moeten we hun kennis zien vast te leggen op een manier die voor henzelf èn voor andere ontwikkelaars bruikbaar is. Voor de ontwikkelaar zelf is dit van belang om, als men een eerder opgelost probleem tegenkomt - het bekende "déjà vu" gevoel - de oplossing zelf ook weer snel te kunnen reconstrueren. Voor andere ontwikkelaars vormt dit soort informatie een leer- en inspiratiebron waar ze in voorkomende gevallen gebruik van kunnen maken.
Documentatie van ervaring is belangrijker naar mate het karakter van een discipline ambachtelijker is. Een goed voorbeeld hiervan is object-georiënteerd ontwikkelen. Voor het maken van goede objectmodellen - met de juiste klassen, overervingsrelaties, associaties tussen objecten en communicatiepatronen - bestaan op dit moment wel veel heuristieken maar niet of nauwelijks formele, meetbare regels. De kwaliteit van een model wordt voorlopig sterk bepaald door de kwaliteit èn de ervaring van de ontwikkelaars.
Het is dan ook niet geheel toevallig dat binnen de object-georiënteerde gemeenschap de afgelopen jaren een nieuw gebied ontstaan is waarin wordt geprobeerd de kennis van ervaren OO ontwikkelaars vast te leggen. Dit gebeurt met zogenaamde patterns, algemene oplossingen voor veel voorkomende OO ontwerp-problemen die worden beschreven in een standaard formaat.
In dit artikel geven we een beknopt overzicht van de stand van zaken rond patterns. We gaan in op de achterliggende gedachten (par. 2) en de rol van patterns in OO ontwikkelen (par. 3). Na een illustratie van het gebruik van patterns (par. 4) gaan we in op het bestaande aanbod van patterns (par. 5) en de ervaringen ermee in de praktijk (par. 6). We besluiten met conclusies en een aantal open vragen (par. 7).
Patterns komen voort uit het werk van de Amerikaan Christopher Alexander op het gebied van architectuur. Alexanders doel was om het ontwerp van "goede" architecturen op allerlei schaalgroottes (van kamers en huizen tot aan wijken, steden of regio's) te stimuleren, zie Alexander(1979). Hiertoe stelde hij een ontwerp-aanpak voor gebaseerd op patterns.
Een pattern geeft een op ervaring gebaseerd advies voor het komen tot een goede oplossing voor een bepaald ontwerpprobleem. In Alexanders gebied praten we dan over het ontwerp van een tuinpad, een verbinding tussen kamers, de plaatsing van ramen in een kamer, maar ook de plaatsing en onderlinge verbinding van wijken in een stad. Elk pattern heeft een identificerende, pakkende naam en is beschreven volgens een vast stramien:
Hoewel beschreven in natuurlijke taal heeft een pattern enige gelijkenis met de regels in kennissystemen: gegeven een bepaalde context en een probleem suggereert het pattern een mogelijke aanpak, waardoor een nieuwe context ontstaat en andere patterns toegepast kunnen worden.
De algemene ontwerp-aanpak met patterns ligt nu voor de hand. Gegeven een specifieke ontwerp-vraag, bijvoorbeeld het toevoegen van een overdekt terras aan een huis, moet een aantal relevante patterns worden geselecteerd en gecombineerd in een zogenaamde "pattern language" die leidt tot een goede oplossing. Een voorwaarde daarvoor is natuurlijk wel dat er een samenhangende catalogus van patterns is waaruit we kunnen putten. Alexander en zijn collega's hebben op basis van hun ervaringen een samenhangende verzameling van 253 patterns van verschillende schaalgroottes ontwikkeld die wordt beschreven in Alexander(1977).
Het is belangrijk om op te merken dat patterns niet automatisch tot één oplossing voor één probleem leiden. In de eerste plaats geeft elk pattern een algemene oplossing die, gegeven de omstandigheden, nader moet worden ingevuld. Daarnaast kunnen er meerdere patterns voor hetzelfde probleem zijn, en kunnen verschillende combinaties van patterns worden samengesteld voor dezelfde ontwerp-vraag. De patterns vormen als het ware de woorden die in verschillende pattern languages kunnen worden gecombineerd.
Sinds het begin van de jaren '90 is er een groeiende belangstelling voor het gebruik van Alexanders patterns bij software-ontwikkeling, zie bijvoorbeeld Lea(1994). Hoewel de benadering natuurlijk toepasbaar is op allerlei gebieden en paradigma's beperken we ons in dit artikel tot het gebruik van patterns bij het ontwerp van object-georiënteerde systemen. Het gaat dan om patterns die breed toepasbare, en in de praktijk beproefde OO oplossingen geven voor bepaalde ontwerp-problemen.
Voor het vastleggen van OO patterns wordt in grote lijnen het eerder besproken stramien gehanteerd. Een pattern heeft dus een pakkende naam, een beschrijving van het probleem en de context, van de oplossing en van de effecten. Vanzelfsprekend zijn de beschrijvingen nu meer toegespitst op OO software. Een oplossing wordt weergegeven in termen van een stukje ontwerp bestaande uit klassen en objecten, methoden en attributen, overervings- en associatie-relaties en communicatiepatronen. Bij de overwegingen over de toepasbaarheid en het gebruik van een pattern wordt vooral aandacht besteed aan de verschillende manieren waarop een pattern geïmplementeerd kan worden (bijvoorbeeld in verschillende OO programmeertalen) en de effecten die het gebruik van het pattern heeft op, bijvoorbeeld, geheugengebruik of executiesnelheid.
Als voorbeeld bespreken we het Observer pattern uit de catalogus in Gamma (1995). Het Observer pattern (ook wel het Publish-Subscribe pattern genoemd) geeft aan hoe we functionele afhankelijkheden tussen objecten kunnen realiseren. We kunnen ermee regelen dat wanneer een bepaald object van toestand verandert, één of meer andere objecten daarover automatisch worden ingelicht. Dit probleem doet zich bijvoorbeeld voor wanneer een applicatieobject als een spreadsheet bepaalde gegevens beheert die op verschillende manieren in een user-interface kunnen worden gepresenteerd (bijvoorbeeld als tabel en als grafiek). Als de toestand van het applicatieobject verandert moeten alle daarvan afhankelijke presentatie-objecten worden ingelicht, de nieuwe toestand opvragen en zichzelf opnieuw tekenen. Het Observer pattern is een onderdeel van de Model-View-Controller (MVC) architectuur voor interactieve applicaties zoals ontwikkeld in Smalltalk, zie Krasner(1988).
Figuur 1: (Vereenvoudigde) structuur van het Observer pattern uit Gamma(1995)
Figuur 1 laat de algemene structuur (in een OMT-achtige notatie) zien van het Observer pattern. Dit model geeft geen concreet ontwerp weer, hoewel dat in eerste instantie wellicht wel zo overkomt. Het gaat erom dat in abstracte termen wordt aangegeven welke rollen, verantwoordelijkheden en interactie er nodig zijn bij het realiseren van afhankelijkheidsbeheer. De klasse Subject, bijvoorbeeld, representeert de rol van het object waarvan anderen afhankelijk zijn. Middels een aanroep van de methode Notify worden de afhankelijke objecten geïnformeerd. De afhankelijke objecten spelen de rol van Observer. Zij moeten zich aan- en afmelden bij het Subject (methodes Attach en Detach) en worden geïnformeerd over toestandsveranderingen via een aanroep van de methode Update. Vervolgens kunnen ze de toestand van het Subject opvragen via de methode GetState().
Als we het Observer pattern gebruiken in een daadwerkelijk ontwerp zullen we de rollen, verantwoordelijkheden en interactie moeten afbeelden naar en integreren met nieuwe en reeds aanwezige klassen en methoden. In "officiële" terminologie betekent het dat we het pattern moeten instantiëren in een ontwerp. Voorkomens van patterns kunnen daarbij overlappen. Het kan bijvoorbeeld gebeuren dat een object een Observer is in één instantie van het Observer pattern, en een Subject is in een andere instantie van hetzelfde pattern. Denk bijvoorbeeld aan beursinformatie die vanuit een centrale computer wordt verspreid naar meerdere werkstations, en vervolgens binnen die werkstations naar verschillende presentatieobjecten.
Een pattern als Observer is abstract in de zin dat er verschillende detailuitwerkingen en implementaties mogelijk zijn. Het afhankelijkheidsmechanisme kan bijvoorbeeld worden ingebouwd in abstracte super-klassen (zoals in Smalltalk is gebeurd) maar ook expliciet worden uitgeprogrammeerd in die klassen waar het nodig is. En bij de uitwerking kunnen we bijvoorbeeld ook extra functionaliteit toevoegen die de hoeveelheid communicatie tussen de objecten beperkt, en de Observers alleen informeert als een serie van toestandswijzigingen in het Subject zijn voltooid.
Dit artikel geeft natuurlijk geen ruimte om veel verschillende patterns te behandelen. Om toch een beeld te geven van het soort problemen dat in patterns wordt opgelost bespreken we een gedeelte van een WWW-browser (à la Netscape). De WWW-browser moet in staat zijn documenten te tonen die bestaan uit verschillende elementen, zoals tekst, afbeeldingen, muziek en verwijzingen naar andere documenten.
De browser moet kunnen communiceren via een lokaal netwerk en het Internet. De informatie die de browser nodig heeft, kan lokaal opgeslagen zijn, op disk of CD-rom staan of alleen via het netwerk benaderbaar zijn. Data die via het netwerk benaderd moet worden en groot van omvang is (plaatjes, video objecten), leidt vaak tot verlies in interactieve performance (bijvoorbeeld bij gebruik van een modem). Dit gebeurt vooral als de data nodeloos wordt opgehaald.
Om de toegang tot dergelijke dure, alleen op verzoek op te halen, data te encapsuleren en beheersen kan het Proxy-pattern gebruikt worden. Kern van dit pattern is het ontwerpen van een plaatsvervangend object (een Proxy) met exact hetzelfde interface als het te benaderen object. Het idee is nu dat 'dure' operaties (die bijvoorbeeld veel tijd of ruimte kosten) pas uitgevoerd hoeven te worden wanneer het echt nodig is. Een afbeelding die veel schijfruimte in beslag neemt, wordt pas opgehaald als de browser (en indirect de gebruiker) daar expliciet om vraagt.
In de pattern-catalogus in Gamma(1995) vinden we de abstracte structuur van het Proxy
pattern zoals weergegeven in figuur 2. Subject geeft de gemeenschappelijke interface voor
Proxy en RealSubject, zodat op elke plaats waar een RealSubject-object kan voorkomen ook
een Proxy-object kan staan.
Figuur 2: Het Proxy pattern uit Gamma(95)
In ons ontwerp kunnen we de rollen Subject, RealSubject en Proxy vervangen door eigen klassen, te weten Graphic, Image en ImageProxy. De operatie Draw is een voorbeeld van een dure operatie; voor de operatie Width hebben we de (binaire) informatie zelf niet nodig; die kan in de proxy zelf snel worden afgehandeld. Dit leidt tot het model dat is weergegeven in figuur 3.
Figuur 3: Afbeelding van een pattern naar een ontwerp
De netwerkverbindingen kunnen op dezelfde manier gemodelleerd worden. Pas als het nodig is, vindt er communicatie over het netwerk plaats.
Een WWW-browser omvat ook andere voorkomens van patterns. Als een document door meer dan één browser wordt bekeken en de inhoud van het document verandert (bijvoorbeeld een beurspagina met actuele koersen) moeten de browsers van de verandering op de hoogte gebracht worden, zodat ze de nieuwe informatie kunnen tonen. Voor dit probleem kunnen we het eerder besproken Observer-pattern gebruiken. Elke browser wordt een 'observer' en abonneert zich op de notificatie-berichten van het subject, het document. Op het moment dat het document veranderd is, wordt aan de abonnees een bericht gestuurd waarin dit wordt gemeld. Ze kunnen dan zelf de benodigde acties ondernemen.
Een soortgelijk probleem vinden we aan de kant van de WWW-server. Als de actuele beursinformatie, bijvoorbeeld opgeslagen in een database, veranderd is, moet de WWW-pagina zelf ook aangepast worden. Het Observer-pattern is hier ook voor de gebruiken.
We hebben al eerder opgemerkt dat het werken met patterns pas effectief mogelijk is als er een catalogus is waarin goede patterns zijn verzameld, het liefst met duidelijke onderlinge samenhang. Inmiddels zijn er de nodige pattern definities verschenen en wordt er hard gewerkt aan meer. Kenmerkend daarbij is dat er veel wordt samengewerkt en gediscussieerd, zowel binnen organisaties als over het Internet. Ervaringen van andere ontwikkelaars leiden telkens tot aanpassingen en verduidelijkingen. In conferenties en workshops op dit gebied worden pattern voorstellen dan ook vaak in kleine groepen besproken, zie Coplien(1995).
Opvallend is dat er patterns zijn ontstaan op verschillende schaalgroottes. Architectuur patterns, bijvoorbeeld, beschrijven globale applicatiearchitecturen. Een voorbeeld hiervan is de Model-View-Controller architectuur voor interactieve applicaties die is ontstaan bij de ontwikkeling van Smalltalk, zie Krasner(1988). Daarnaast zijn er patterns die specifiek gericht zijn op de stijl van programmeren in bepaalde programmeertalen, en die aangeven hoe van bepaalde constructies in die talen gebruik moet worden gemaakt. Dergelijke patterns worden ook wel idiomen genoemd, zie bijvoorbeeld Coplien(1992).
Tenslotte zijn er de design patterns die middelgrote patronen beschrijven; ontwerp fragmenten die in allerlei soorten systemen op allerlei domeinen, terugkomen. Een eerste mijlpaal op dit terrein is gezet door de auteurs van het boek "Design Patterns - Elements of Reusable Object-Oriented Software", zie Gamma(1995). Zij beschrijven 23 patterns voor OO ontwerp in detail, waaronder de eerder genoemde Observer en Proxy patterns. Conform de pattern filosofie staat er - volgens de auteurs - in dit boek niet iets fundamenteel "nieuws". De patterns zijn dan ook niet zozeer ontwikkeld als wel "ontdekt", hetgeen aangetoond wordt door verschillende voorkomens van een pattern in de praktijk te bespreken.
De meeste bestaande OO patterns definiëren de kwaliteit van een oplossing ("het goede") in termen van flexibiliteit en onderhoudbaarheid. Het is dan ook niet verrassend dat algemenere, maar minder concrete, ontwerp-principes voor flexibele en onderhoudbare systemen veel worden toegepast. Principes als het lokaliseren van beslissingen, het scheiden van "policies" en mechanismen, het loskoppelen van abstracties en hun implementatie, of de reductie van afhankelijkheden tussen objecten zijn vaak herkenbaar in de voorgestelde oplossingen.
Hoewel het gebied nog in de kinderschoenen staat, zijn er al enkele redelijk grote software-projecten uitgevoerd met behulp van design patterns. Patterns blijken goed toegepast te kunnen worden in communicatie-software, gedistribueerde systemen en grafische programmatuur. Onder andere bij Motorola, Ericsson, Kodak, Siemens en AT&T zijn ervaringen opgedaan met het gebruik van patterns in het ontwikkeltraject. De belangrijkste observaties zijn:
Patterns vormen een interessant nieuw gebied. Er wordt actief geprobeerd de ervaringen van goede (OO) ontwikkelaars te inventariseren en te documenteren, waarbij het pattern formaat een attractieve vorm blijkt te zijn. Hierdoor wordt niet alleen hergebruik gestimuleerd, maar wordt het ook mogelijk om op een wat hoger abstractieniveau te denken en communiceren over OO software. Gegeven de alsmaar toenemende omvang en complexiteit van moderne computertoepassingen is dit een niet te onderschatten voordeel.
Hoewel er vrij veel patterns voor OO systemen zijn (en worden!) gedefinieerd, zijn er momenteel nog geen grote, goed samenhangende catalogi. Dit is geen verrassing, omdat het gebied nog in zijn kinderschoenen staat. Hoewel er wel gedacht wordt over grotere, systematische catalogi, zal het nog wel een paar jaar duren voordat we zover zijn. Dit vormt wel een hindernis voor de gebruiker van patterns, en met name voor onervaren OO ontwikkelaars. Het vinden van de juiste patterns voor een probleem wordt door het gebrek aan samenhang bemoeilijkt. Men moet de beschikbare patterns kennen en op waarde kunnen schatten.
De pattern gedachte is op zich breed toepasbaar. James Coplien, bijvoorbeeld, heeft een pattern language opgesteld voor het opzetten van ontwikkelorganisaties en ontwikkelprocessen, zie zijn bijdrage in Coplien(1995). Ook binnen het terrein van OO ontwikkelen is variatie mogelijk. Er valt te denken aan domeinspecifieke patterns, bijvoorbeeld voor telecommunicatie software als beschreven in Schmidt(1995), maar ook aan patterns die de conventies binnen een ontwikkelorganisatie beschrijven. Het "boven tafel halen" van dergelijke patterns vergt - zoals eerder reeds opgemerkt - de nodige tijd en veel discussie. Goede regels voor het opstellen van patterns zijn er nog niet, laat staan dat er een "pattern language" is voor het samenstellen van patterns.
Patterns maken het mogelijk om op een wat hoger abstractieniveau te kunnen denken over OO systemen. Een voor de hand liggende vraag is dan ook of er een ontwerphulpmiddel te maken is waarmee ontwikkelaars op het niveau van patterns systemen kunnen ontwerpen. Voorwaarde hiervoor is dat er een meta-model is, waarin de oplossingsstructuur van een pattern kan worden weergegeven en dat kan worden afgebeeld naar daadwerkelijke klassen en hun relaties, eventueel in verschillende programmeertalen. Eerder werk op het gebied van OO contracten, zie Helm(1990), moet daartoe worden gegeneraliseerd in een flexibele omgeving waarin op verschillende abstractieniveaus kan worden gewerkt. Dit is voorlopig dus nog een onderwerp voor onderzoek, waaraan o.a. bij de Vakgroep Informatica van de Universiteit Utrecht wordt gewerkt.