(Proceedings ASI Seminar: Object-oriëntatie Kritisch Bekeken, Juni 1994)
Object-oriëntatie (OO) is de laatste jaren een zeer populaire marketing term geworden. Het begint een beetje te lijken op het woord "groen" in de wasmiddelen reclames: als het maar OO is, dan is het goed. Zonder dat duidelijk gemaakt wordt wat er nu precies zo object-georiënteerd is aan een produkt of een benadering (en vaak is dat bijzonder weinig), wordt geprobeerd een link te leggen met allerlei voordelen die met object-oriëntatie worden geassocieerd.
De overdaad aan "hype" zorgt voor de nodige verwarring rond de mogelijkheden en beperkingen van OO. Object-oriëntatie is geen "silver bullet" die alle automatiseringsproblemen zo maar even oplost. Het is een ander paradigma voor het ontwerpen en bouwen van software dat concrete voordelen kan opleveren zoals hergebruik van software, aanpasbare en flexibele systemen, verbeterde kwaliteit van produkten, en vereenvoudiging van onderhoud. Deze voordelen worden echter niet zo maar bereikt. Daar is meer voor nodig dan het gebruik van een OO-tool of een OO-taal. Het vereist bijvoorbeeld een omschakeling in de denkwijze en werkwijze van ontwikkelaars, maar ook veranderingen in de organisatie van projecten. Het overstappen naar OO ontwikkelen vergt investeringen (cursussen, experimenten, technologie verkenningen, etc.) terwijl de echte voordelen pas later komen.
Toch wegen de voordelen van OO meestal op tegen de initiële investeringen. In dit artikel willen we dit nog eens onderstrepen door te laten zien hoe de principes van object-georiënteerd ontwikkelen (hoofdstuk 2) ook op architectuurniveau (hoofdstuk 3) - het niveau van complexe systemen en hun onderverdeling in subsystemen - kunnen worden toegepast (hoofdstuk 4). We gaan daarbij onder andere in op het modelleren van OO-systemen (hoofdstuk 5) en standaard-architecturen (hoofdstuk 6). Tevens bespreken we een paar stukken technologie die beschikbaar zijn voor het afbeelden van een OO architectuur naar een OO implementatie (hoofdstuk 7).
Object-oriëntatie is eind jaren zestig ontstaan als programmeertechniek. De taal Simula-67 - gericht op simulaties - was de eerste programmeertaal die ondersteuning bood voor het definiëren van klassen en het creëren van objecten. In de jaren zeventig gingen de ontwikkelingen op beperkte schaal verder, o.a. bij Xerox PARC waar de taal Smalltalk werd gedefinieerd. Pas in de jaren tachtig sloeg OO echt aan. Met de introductie van Smalltalk-80 [Goldberg85] en andere talen als C++ [Stroustrup87] en Eiffel [Meyer88], begonnen steeds meer mensen geïnteresseerd te raken in het idee van objecten. Tevens werden de relaties met andere ontwikkelingen uit de jaren 70 zoals modularisatie en abstracte data types duidelijker. Naast introductie van programmeertalen en omgevingen werd ook een begin gemaakt met onderzoek naar ontwikkelmethoden voor object-georiënteerd systeem ontwikkeling. Ook werden (en worden) bepaalde ideeën uit de OO wereld toegepast in nieuwe (soorten) tools, variërend van databases tot grafische user-interface pakketten en 4 GL's.
Het centrale idee bij OO is dat een systeem bestaat uit samenwerkende objecten. Objecten verenigen toestand (data) en gedrag (operaties) in één concept. Ieder object heeft bepaalde interne variabelen waarin data is opgeslagen. Verder is er een verzameling operaties (of methoden) die andere objecten kunnen aanroepen om iets met het object te doen. De methoden zijn een soort procedures die toegang hebben tot de interne variabelen van een object. Maar methoden kunnen ook weer methoden aanroepen van andere objecten. Het aanroepen van een methode van een object wordt ook wel het versturen van een bericht genoemd.
In de meeste talen en omgevingen worden objecten beschreven door klassen. Een klasse beschrijft zowel de structuur als de operaties voor een bepaald soort objecten. In de klasse wordt vastgelegd welke interne variabelen een object van die klasse kent, en welke methoden er zijn en hoe die geïmplementeerd zijn. Daadwerkelijke objecten zijn nu instanties van zo'n klasse. Ze bevatten waarden voor de verschillende variabelen die in de klasse zijn gedefinieerd. Op een instantie kunnen de operaties die in de klasse zijn gedefinieerd worden uitgevoerd. Bij de executie van zo'n methode is het object waarop de methode wordt aangeroepen een (impliciete) parameter. Als voorbeeld kunnen we ons de klasse "button" voorstellen met de bijbehorende instantie-variabelen (positie, tekst, kleur, etc.) en operaties (maak zichtbaar, verplaats, etc.). Instanties van deze klasse (de objecten) zijn dan buttons die we daadwerkelijk op het scherm kunnen zien en manipuleren.
De moderne kijk op objecten is sterk beïnvloed door de eerdere ontwikkelingen rond modularisatie [Parnas72] en abstracte data types [Liskov77]. Het eerste sleutelwoord is encapsulatie. Een object heeft nooit rechtstreeks toegang tot de interne variabelen van een ander object. Het heeft alleen toegang tot de methoden die voor het object zijn gedefinieerd. Als er iets in de toestand van een object moet worden veranderd, dan moet dat dus altijd via de aanroep van zo'n methode gaan, dus via het versturen van een bericht naar het object.
Net als bij modules is het daarbij handig om onderscheid te maken tussen de interface, ofwel de methoden die extern zichtbaar zijn en die dus door andere objecten kunnen worden aangeroepen, en de implementatie, de interne methoden die voor de realisatie van die interface nodig zijn maar die niet voor buitenstaanders toegankelijk zijn. De resulterende ingrediënten van een klasse zijn weergegeven in figuur 1.

Figuur 1: Ingrediënten van een klasse (-definitie).
Het grote voordeel van encapsulatie is dat - zolang de interface van extern zichtbare methoden maar hetzelfde blijft - de implementatie en representatie van een klasse aangepast kan worden zonder dat andere delen in het systeem hier last van hebben. Daarom is het belangrijk bij het ontwerpen van de interface methoden van klassen te streven naar abstractie. Dat betekent dat de zichtbare operaties geen "hints" geven over hoe ze toevallig zijn geïmplementeerd. In de woorden van Parnas [Parnas72, Parnas79]: een interface moet zodanig zijn dat alle essentiële functionaliteit voor de klanten beschikbaar is, terwijl voor de implementator zo veel mogelijk keuzevrijheid bestaat om een implementatie te kiezen.
Via overerving of inheritance definiëren we een nieuwe klasse die een subklasse is van een bestaande klasse. In de subklasse kunnen we extra instantie-variabelen introduceren, maar ook nieuwe methoden toevoegen en (soms) zelfs bestaande methoden anders implementeren. Instanties van de subklasse bevatten nu waardes voor de variabelen uit de subklasse en de superklasse. Ook kunnen de zichtbare methoden uit beide klassen op zo'n object worden uitgevoerd.
Inheritance is een techniek die op vele manieren kan worden gebruikt en misbruikt. We kunnen het bijvoorbeeld gebruiken om voor een klasse die alleen een interface aan zichtbare operaties definieert (een abstracte klasse) een realisatie te definiëren in een subklasse (zie ook figuur 2.). In de subklasse geven we een implementatie voor alle interface methoden en stellen we vast welke gegevens er in instanties worden bijgehouden. Van deze subklasse kunnen dan wel instanties worden gemaakt, het is een zgn. concrete klasse.

Figuur 2: Klassen, instanties, inheritance en verwijzingen
Een standaard voorbeeld van een abstracte klasse is de klasse "Collection" in Smalltalk. Deze klasse definieert de operaties die op elk soort collectie zijn uit te voeren zonder dat er een implementatie wordt gegeven. Typische operaties die hier zijn beschreven zijn: "bevat de collectie een bepaald object" of "geef het aantal elementen in de collectie". Concrete subklassen van "Collection" zijn bijvoorbeeld "Array" of "Set". Zij bieden implementaties voor de operaties gedefinieerd op "Collection", plus extra operaties die specifiek zijn voor deze klassen. Als een object nu verwacht te communiceren met een "Collection" object dan kan het de operaties versturen die gedefinieerd zijn in de interface van de klasse "Collection", bijvoorbeeld "geeft het aantal elementen". Als het werkelijke object waarmee wordt gecommuniceerd wordt nu van de klasse "Array" is, dan zal via dynamic binding de implementatie voor de methode uit de klasse "Array" worden gekozen.
Vanuit het streven naar aanpasbaarheid is het principe van specialisatie belangrijk. Dit betekent dat objecten van een subklasse te allen tijde moeten zijn te behandelen als objecten van de superklasse. Dit betekent dat een subklasse tenminste dezelfde zichtbare operaties heeft als de superklasse en dat die operaties ook op dezelfde manier werken als gedefinieerd in de superklasse. Hierdoor hoeven alle bestaande objecten die waren ingesteld op het gedrag van de superklasse niet te worden aangepast als ze met een object van de subklasse te maken krijgen.
Een belangrijk kenmerk van objecten tenslotte is, dat ze een identiteit hebben. Dit zorgt ervoor dat twee objecten die instanties zijn van dezelfde klasse en ook dezelfde waardes hebben voor de interne variabelen, toch verschillende objecten zijn. Hiervoor hoeven we geen volgnummers o.i.d. te gebruiken. Identiteit is een dienst die door de omgeving waar objecten zijn gecreëerd wordt geleverd. De identiteit van een object is ook van belang om objecten onderling te koppelen. Door in een interne variabele van een object de identiteit van een ander object op te nemen kunnen we verwijzingen of referenties tussen objecten creëren. Verder kunnen we zo vanuit het ene object berichten sturen naar het andere.
Het combineren van data en operaties tot een geheel maakt van objecten bouwstenen. In plaats dat we de werking en de data van een systeem volledig gescheiden beschrijven - zoals bij traditionele benaderingen - doen we het hier per object (of eigenlijk per klasse). Als we iets moeten veranderen in een object-georiënteerd systeem hoeven we dus niet het hele programma te veranderen of te bekijken, maar kunnen we ons veelal beperken tot individuele klassen. En dit kenmerk maakt object-georiënteerde systemen veelal beter aanpasbaar en onderhoudbaar en dus beter geschikt voor incrementele en evolutionaire ontwikkeling.
Goed gebruik van principes als encapsulatie en abstractie bevordert de aanpasbaarheid omdat de kans groter is dat de zichtbare interface aan methoden niet verandert als de implementatie wordt aangepast. Zoals gezien kunnen we zelfs verschillende implementaties voor dezelfde interface hebben via de definitie van subklassen en het principe van specialisatie. Hierbij is het overigens wel van belang dat de afhankelijkheden tussen klassen (of objecten) worden beperkt. Dit betekent een reductie van de "coupling" een vergroting van de "cohesie", bijvoorbeeld door het gebruik van heuristieken als de Law of Demeter [Lieberherr88, Lieberherr89]. Reductie van koppeling en lokaliseren van ontwerp-beslissingen vergroot de flexibiliteit en herbruikbaarheid. Door het kiezen van verschillende implementaties of combinaties kunnen we eenvoudig verschillende varianten of configuraties van één programma maken. Zoals reeds in de jaren 70 door Parnas is opgemerkt [Parnas72, Parnas76]: we ontwerpen niet één programma, maar een "familie van programma's".
Een ander voordeel is de traceerbaarheid door de fasen in een ontwikkeltraject. In de analyse fase kunnen we (in hele globale termen) al klassen identificeren. In de ontwerpfase beschrijven we voor die klassen hoe ze zouden moeten werken, terwijl we ze in de implementatie echt uitwerken. We hebben één concept dat op verschillende abstractie- en beschrijvingsniveaus kan worden gehanteerd.
Binnen een object-georiënteerd systeem is hergebruik op verschillende manieren mogelijk. Inheritance is de meest aansprekende manier. Daarnaast kunnen we gebruik maken van reeds bestaande klassen. In de meeste talen is bijvoorbeeld wel een bibliotheek met klassen zoals Lijsten of Verzamelingen beschikbaar. Nu is het erg gemakkelijk geworden om bijv. in een object van de klasse persoon een "lijst-object" op te slaan waarin een aantal "adres-objecten" zitten die overeenkomen met (verwijzingen naar) de adressen van een persoon.
De begrippen die hier kort zijn besproken (object en klasse, identiteit, methoden en berichten, inheritance) vormen de basis van de object-georiënteerde benadering. Over de details en de precieze betekenis van constructies verschillen de meningen nogal eens. Zo zijn er nog steeds heftige discussies of meervoudige overerving (erven van meerdere klassen) nu wel of niet goed is. Belangrijk is verder om op te merken dat het bij OO gaat om een benadering die goed en slecht kan worden toegepast. Als we het goed doen krijgen we inderdaad beter gestructureerde, aanpasbare en onderhoudbare systemen. Echter op de vraag wat we dan moeten doen om dit te garanderen is (nog) geen eenduidig antwoord. Er zijn weliswaar verschillende OO analyse en ontwerpmethoden, maar deze geven ze geen automatische garantie dat de "goede objecten" worden gevonden. Voorlopig blijft het wat dat betreft bij heuristieken en ervaring [Johnson88].
Gegeven de oorsprong van OO is het niet toevallig dat er veel aandacht is en wordt besteed aan OO programmeren, en dus ook aan OO programmeertalen en -omgevingen. De belangstelling voor OO analyse en ontwerp is bijvoorbeeld van veel recenter datum. De problemen bij de bouw van moderne systemen liggen echter niet zozeer in de constructie van geïsoleerde programma's, maar veeleer in het ontwerpen en bouwen van complexe systemen die bestaan uit diverse samenwerkende onderdelen en deelsystemen - zoals een grafische user interface, een kennisbank, complexe berekeningen, of een databank. Bij de bouw van dit soort systemen zien we vaak de volgende zaken naar voren komen:
Het is duidelijk dat het bouwen van systemen met dit soort kenmerken geen eenvoudige zaak is. Een van de manieren om hier wat meer grip op te krijgen is door te kijken naar de architectuur van het systeem. Het idee hierbij is dat we de werking van grote systemen op een hoog abstractieniveau beschouwen en beschrijven en daarbij zo min mogelijk rekening houden met de details van de uiteindelijke implementatie. Het doel is om de essentie van het systeem (de functionaliteit, de opdeling in onderdelen, de manier waarop ze communiceren, etc.) boven tafel te krijgen en om hierover te kunnen redeneren zonder te verzanden in implementatiedetails. Een architectuur kan dan ook op meerdere manieren worden vertaald in een implementatie.
Deze definitie van software architectuur is noodgedwongen nogal vaag. Dit komt omdat het denken over software architectuur nog in de kinderschoenen staat; de eerste stappen zijn pas aan een paar jaar geleden gezet [Shaw89, Perry92]. Maar inmiddels wordt er al wel gewerkt op verschillende fronten. Zo wordt er bijvoorbeeld onderzoek verricht naar talen en technieken die ons in staat stellen een architectuur te beschrijven [Buhr92, Allen92]. Verder wordt er gekeken naar "standaard" architecturen die regelmatig (en in verschillende gebieden) terugkomen [Shaw89, Allen92]. Ook wordt gezocht naar architectonische vuistregels die bij de constructie van systemen kunnen worden gehanteerd [Johnson92, Love91], o.a. in navolging van het werk rond pattern-languages van Christopher Alexander op het gebied van stedebouw en huizenbouw [Alexander77, Alexander79].
Het gebied van de software architectuur is geïnspireerd door de "traditionele" architectuur van gebouwen. Daar is men goed in staat om een systeem (het gebouw) op verschillende niveaus van detail en vanuit verschillende gezichtspunten te beschrijven en om in een vroeg stadium uitspraken te doen over het constructieproces. Een architectuurbeschrijving blijkt ook later nog nuttig, bijvoorbeeld bij het installeren van nieuwe apparatuur of bij een verbouwing. Binnen de reguliere architectuur is hergebruik ook normaal. De afmetingen van de gaten in muren worden afgestemd op standaard beschikbare ramen en deuren. Bij het ontwerp wordt al rekening gehouden met hergebruik tijdens constructie.
Dit soort dingen willen we natuurlijk ook graag voor software systemen. Zo zou het handig zijn om te weten wat de "dragende constructie" van een systeem is en wat alleen maar "decoratie" is. Dit maakt onderhoud (of verbouwing) in een later stadium ook een stuk gemakkelijker. Verder zijn standaard architecturen en standaard raamwerken waarin (herbruikbare) onderdelen eenvoudig kunnen worden ingeplugd belangrijk omdat ze de ontwikkeling van systemen aanmerkelijk kunnen versnellen. Het actuele voorbeeld hiervan is de client-server architectuur bestaande uit een grafische user-interface als client en een relationele database als server. Doordat tools van deze architectuur uitgaan is het ontwikkelen van dit soort systemen aanmerkelijk gemakkelijker geworden.
Hoewel de architectuur van gebouwen een goede inspiratiebron kan zijn, moeten we niet vergeten dat software systemen dynamische aspecten hebben. Tijdens executie vinden allerlei (tijdafhankelijke) gebeurtenissen en veranderingen plaats. Het vastleggen van deze dynamiek vereist meer dan de beschrijvingen van de statische structuur zoals we die uit de bouw kennen. Daarbij komt dat de omgeving van een software systeem veel minder stabiel is dan de omgeving waar een normale architect mee te maken heeft. Terwijl aansluitingen op rioleringen, stroom of telefoon zijn gestandaardiseerd hebben we bij software te maken met continue aanpassingen van systeemsoftware. Ook is het bij software nog steeds normaal dat de eigenschappen van het bouwmateriaal (bijvoorbeeld de programmeertaal) in de loop van de tijd veranderen.
Het object-georiënteerde paradigma biedt een interessante basis voor het beschrijven van software architecturen. Zoals we hebben gezien draait het bij OO om objecten die onderling samenwerken via het versturen van berichten. Van ieder object (of eigenlijk klasse) is alleen de interface zichtbaar. De implementatie van de interface en de representatie van de gegevens zijn verstopt.
Het is intuïtief duidelijk dat we dit idee ook kunnen gebruiken voor de beschrijving van een systeem dat bestaat uit verschillende componenten. Deze componenten werken samen doordat de ene (in abstracte zin) een bericht verstuurt naar de andere. Bij elke component is er (in abstracte zin) sprake van een interface aan primitieven die de component biedt en een onzichtbare implementatie van die interface. Ook is hier het onderscheid tussen klasse en instantie te hanteren. Binnen een draaiend systeem kunnen er natuurlijk meerdere instanties (objecten) zijn van dezelfde klasse, bijvoorbeeld als er meerdere instanties van de user-interface actief zijn die werken op dezelfde applicatie of database.
Op architectuurniveau doet het er verder niet toe in welke talen of systemen de componenten zijn gerealiseerd of via welk mechanisme uiteindelijk vanuit de ene component een operatie van de andere wordt aangeroepen (een normale procedure aanroep, een remote procedure call, een DDE-primitieve, etc.). Ook doet het er - in principe - niet toe hoe de verschillende componenten zijn geconfigureerd in programma's of zijn verspreid over computers. Hierop kunnen immers de gekozen mechanismen voor berichtenverkeer worden aangepast. In principe kunnen voor één architectuur dus meerdere implementaties worden gecreëerd. Verder is het goed voorstelbaar dat er uiteindelijk tools komen waarmee een hele architectuur semi-automatisch af te beelden is op een implementatie, waarbij de benodigde mechanismen voor communicatie tussen componenten automatisch worden ingevuld.
Toch spelen de karakteristieken van een implementatieomgeving wel degelijk een rol bij de omzetting van een architectuur naar een technisch ontwerp en een implementatie. En dit gaat verder dan de syntax van de berichtenprimitieven of de manier waarop data tussen verschillende talen uitgewisseld kan worden. Wanneer de onderdelen uit een architectuur worden ondergebracht in verschillende processen op verschillende systemen dan moeten we rekening houden met het feit dat netwerkverbindingen kunnen wegvallen en berichten kunnen verdwijnen. Ook kan de snelheid van communicatielijnen een belangrijke rol spelen bij de keuze om bepaalde componenten bij elkaar te voegen of meer van elkaar te laten weten (bijvoorbeeld een afweging tussen stateful vs. stateless servers). Het omzetten van een architectuur naar een goede implementatie is dus voorlopig geen automatisch proces!
Het gebruik van het OO paradigma voor het beschrijven van architecturen betekent dat we dezelfde ontwerp-principes (encapsulatie, abstractie, specialisatie, lokaliseren van ontwerp-beslissingen, etc.) op verschillende schalen kunnen toepassen. Het betekent ook dat de voordelen die OO biedt (zoals hergebruik, aanpasbaarheid en configureerbaarheid) in principe ook gelden voor deze grote bouwstenen.
Omdat we bij software architectuur geïnteresseerd zijn in het beschrijven van de werking van complexe systemen is het niet voldoende om alleen maar klassen en hun interfaces vast te leggen. Dit geeft immers slechts de statische structuur weer. We moeten meer kunnen specificeren over objecten (c.q. klassen), de manier waarop ze samenwerken, en de manier waarop parallelle activiteiten worden gecoördineerd. Daarbij we streven naar technieken die formele beschrijving en analyse van een architectuur mogelijk maken. Ook automatische controle van bepaalde aspecten (bijv. compatibiliteit van samenwerkende objecten) is gewenst, net als (automatisch) controleerbare ontwerpregels die de kwaliteit van een architectuur bevorderen. Deze onderwerpen krijgt de laatste jaren veel aandacht in het onderzoek naar object-georiënteerde systemen. We zullen hier een paar voorbeelden aangeven van recente ontwikkelingen. Voor nadere details wordt verwezen naar de literatuur.
De al eerder genoemde Law of Demeter [Lieberherr88, Lieberherr89] is een ontwerpregel voor de reductie van koppelingen en afhankelijkheden tussen objecten of klassen. Hierdoor wordt een ontwerp eenvoudiger aanpasbaar. Er zijn verschillende varianten, hier beperken we ons tot de klasse-versie. Voor iedere methode M van elke klasse C definieert de wet de verzameling klassen waarnaar vanuit M berichten mogen worden verstuurd. Deze verzameling bestaat grotendeels uit de klassen van de instantie-variabelen gedefinieerd in C en de klassen van de argumenten van M. Berichten naar andere klassen mogen niet worden verstuurd. De wet is te formuleren voor verschillende talen en een OO ontwerp is zelfs automatisch om te zetten om aan de wet te voldoen.
Via de ontwikkeling van verschillende programmeertalen is aangetoond dat parallel programmeren eenvoudig en elegant is te combineren met de object-benadering. In principe is ieder object te beschouwen als een parallel executerend proces dat verzoeken van andere objecten afhandelt. Om nog meer parallellisme te introduceren wordt daarbij soms ook het versturen van het bericht losgekoppeld van het ontvangen van het antwoord. De "client" kan in de tussentijd nog andere dingen doen.
Controle of objecten compatibel zijn vereist meer informatie dan alleen kennis van de interface methoden en typecontrole. We moeten meer kunnen vastleggen over de manier waarop objecten werken en hoe ze gebruikt moeten worden. In de taal Eiffel [Meyer88] bijvoorbeeld kunnen o.a. pre- en postcondities per methode en invarianten per klasse worden gedefinieerd. Bij inheritance wordt specialisatie afgedwongen doordat de subklasse dezelfde methoden moet bieden die tenminste voldoen aan de pre- en postcondities die in een superklasse zijn gedefinieerd.
Een andere benadering hiervoor zijn protocols [Bos89, Laffra92]. Via een protocol wordt beschreven in welke volgorde de zichtbare methoden van een klasse kunnen/moeten worden aangeroepen om zinvol van de functionaliteit van een object gebruik te kunnen maken. Zo moet een file eerst geopend worden voor er uit gelezen of naar geschreven kan worden. Zo'n protocol is voor (de ontwikkelaar van) een client object belangrijk om te weten en wordt veelal vastgelegd in een state-diagrams. Automatische controle van protocol-specialisatie bij inheritance is daarbij ook mogelijk [Nierstrasz93].
Een ontwerpbenadering die sterker gericht is op de interactie tussen objecten zijn de zogenaamde contracts [Helm90, Holland92]. In een contract wordt - in abstracte termen - vastgelegd wat het berichtenverkeer is tussen een aantal objecten die samenwerken voor een bepaald doel. Voor iedere gebeurtenis wordt vastgelegd hoe een participerend object daarop moet reageren. Een typisch voorbeeld is de relatie tussen een grafisch object als een scroll-bar en het bijbehorende tekstvenster dat een deel-view geeft op een document. In een contract kunnen we vastleggen welke berichten worden verstuurd als de scroll-bar wordt verplaatst en hoe de andere objecten (bijvoorbeeld de view) daarop moeten reageren. Bij het daadwerkelijk koppelen van objecten in zo'n verband kunnen we nu (semi-) automatisch controleren of deze objecten voldoen aan de eisen in het contract. Ook is het mogelijk dat op basis van een contract methoden worden gegenereerd voor klassen [Holland92].
Een van de doelen van software architectuur is het inventariseren en beschrijven van standaard architecturen die in verschillende systemen terugkomen. Het resultaat hiervan zijn raamwerken. Ontwikkeling van een systeem dat gebaseerd is op zo'n raamwerk kan vervolgens via standaard technologie ondersteund worden.
In de literatuur zijn inmiddels verschillende architecturen (formeel) beschreven zoals het pipe-line/filter model [Allen92], en gelaagde en blackboard systemen [Shaw89]. Ook binnen de OO wereld is er de nodige aandacht voor deze invalshoek. Naast domein-afhankelijke raamwerken [Johnson92] begint er zo langzamerhand een beeld te ontstaan over de algemene architectuur van OO informatie-systemen.

Figuur 3: 3-niveau architectuur van een OO informatiesysteem
Figuur 3 beschrijft een simpele maar algemene architectuur die bestaat uit 3 delen. De user-interface vormt de buitenkant van het systeem. Het presenteert de informatie aan de gebruikers en draagt zorg voor de interactie met de gebruikers. Ook houdt het de toestand bij die nodig is om met de andere systeemdelen samen te werken. De user-interface communiceert met het applicatiemodel waarin de werkelijke functionaliteit is gerealiseerd. Het applicatiemodel is weer een component die een interface aan operaties biedt. Deze operaties worden afgebeeld op berichten die worden doorgestuurd naar objecten waarvan de klassen binnen de applicatiecomponent zijn gedefinieerd (het zogenaamde domein-model). Hierbij is dus sprake van objecten op twee niveaus! Om de objecten in de applicatiecomponent te kunnen bewaren is een vorm van persistente gegevensopslag nodig. Ook dit is gemodelleerd als een aparte component met een eigen interface. Deze component handelt verzoeken af om objecten uit het applicatiemodel op te halen c.q. op te slaan op disk.

Figuur 4: Voorbeelden van uitbreidingen en aanpassing aan de architectuur
Dit model is inmiddels in verschillende projecten toegepast en geëvalueerd [Florijn93b, Oosterom94]. Het model kan op allerlei manieren worden uitgebreid en aangepast (zie figuur 4), waarbij de voordelen van de object-georiënteerde benadering goed aan het licht komen. Een simpele uitbreiding is bijvoorbeeld dat er verschillende user-interfaces kunnen worden gedefinieerd die werken op hetzelfde applicatiemodel. Ook kan de persistente opslag op verschillende manier worden gedaan, bijvoorbeeld via een object-georiënteerde database of via een afbeelding naar relationele databases. Verder is het belangrijk op te merken dat de structuur van het applicatiemodel arbitrair complex kan zijn. Het kan bijvoorbeeld weer bestaan uit allerlei deelsystemen en componenten die onderling samenwerken. Er kunnen zelfs meerdere bestaande domein-modellen (of zelfs applicatie-modellen) in worden gebruikt. Deze interne complexiteit is echter niet zichtbaar voor de user-interface, want die heeft alleen te maken met de interface van de hele applicatiecomponent.
Bij het omzetten van een architectuur naar een implementatie willen we de voordelen op architectuurniveau graag behouden. Dit betekent dat de componenten uit de architectuur ook terugkomen in de implementatie. Dit vereist geschikte technologie om componenten en berichtenverkeer te implementeren.
De OO evaluatieprojecten die door het SERC zijn uitgevoerd [Florijn93b, Oosterom94] laten zien dat er nog steeds gezocht wordt naar goede, geïntegreerde OO-4GL omgevingen waarmee we zo'n architectuur als in figuur 3 ook eenvoudig kunnen realiseren. Meestal moet het ontwerp worden omgezet naar een implementatie die bestaat uit twee delen, nl. een user-interface (gemaakt met een GUI-builder) en een (relationele) database. Het applicatiemodel verdwijnt als aparte component; de functionaliteit ervan wordt ofwel in de user-interface ingebouwd, of in een verrijkte database. Hierdoor wordt dus wel ingeboet aan aanpasbaarheid, herbruikbaarheid en configureerbaarheid.
Het bouwen van een implementatie met een structuur zoals in figuur 3 vergt voorlopig dus nog het aan elkaar plakken van apart ontwikkelde componenten. Gegeven het belang van distributie en heterogeniteit speelt hierbij de standaardisatie van een open en gedistribueerde berichten-bus een belangrijke rol. Daarom gaan we hieronder kort in op het CORBA model van de OMG. Verder zijn er talloze tools om de verschillende onderdelen uit het plaatje van figuur 3 te helpen bouwen zoals OO programmeertalen, grafische user-interface builders en OO analyse en design tools. Deze zullen we hier niet bespreken. Wel gaan we kort in op de persistente opslag van objecten, o.a. via OO databases.
CORBA, de Common Object Request Broker Architecture biedt een basis voor communicatie tussen separaat ontwikkelde componenten in een heterogene gedistribueerde omgeving. CORBA is ontwikkeld door de Object Management Group, een consortium dat streeft naar standaardisatie van object-technologie ten behoeve van interactie tussen applicaties.
Het CORBA model [OMG90] biedt een raamwerk voor het vastleggen van interface definities van, en primitieven voor berichtenverkeer tussen communicerende entiteiten. Een interface in CORBA bestaat uit een specificatie van operaties die op een bepaald soort objecten aangeroepen kunnen worden. Het model is object-geörienteerd in de zin dat een interface definities kan erven van een andere interface. In CORBA is communicatie gespecificeerd als Remote Procedure Call (RPC)in de context van een client/server model. Een client doet een RPC door een operatie aan te roepen op een object dat wordt geïdentificeerd door een object-referentie. Bij zo'n referentie hoort ook een bepaalde interface definitie. Een request wordt afgehandeld door een server of een implementatie. De vorm van zo'n server wordt in het midden gelaten, dit kan zowel een echt server proces als een library routine in het client proces zijn. De implementatie van de afhandeling van een request is hierdoor transparant.

Figuur 5: CORBA Model
De combinatie van een object en operatie die op dat object wordt uitgevoerd betekent een verzoek om een service middels een request. Een request wordt geaccepteerd door een ORB, een Object Request Broker, die uitzoekt welke implementatie voor het request gebruikt dient te worden. Er kunnen meerdere ORB's (instanties) in een netwerk bestaan die onderling communiceren. De ORB is verantwoordelijk voor het verder versturen van het request naar de implementatie (evt. via andere ORB's) en voor het retourneren van het resultaat.
Het voordeel van het CORBA model is dat het allerlei problemen rond distributie en heterogeniteit verstopt (o.a. via een stub-generator die problemen rond data formaten voorkomt). Voor de client doet het er niet waar de implementatie van een operatie zich bevindt of in welke taal die implementatie is geschreven. CORBA zorgt voor het lokaliseren van de implementatie, en biedt een standaard voor het representeren van data die op verschillende talen afbeeldbaar is.
Het belang van CORBA ligt vooral in de leveranciersonafhankelijkheid van de standaard. De OMG streeft ernaar haar standaards via consensus te laten groeien om zo de acceptatie ervan te bevorderen. Inmiddels hebben verschillende leveranciers een CORBA implementatie beschikbaar of geannonceerd. Verdere standaardisatie van het CORBA model is in volle gang, o.a. om de samenwerking tussen ORB's van verschillende leveranciers te regelen.
In de meeste systemen hebben we te maken met persistente gegevensopslag: er zijn gegevens zijn die tussen de sessies van een applicatie bewaard moeten worden. Meestal gebruiken we hiervoor een standaard database management systemen, zodat zaken als beveiliging, consistentie en concurrency control geregeld zijn.
In een object-georiënteerde applicatie zijn gegevens gerepresenteerd als objecten, en dus zullen we deze objecten moeten opslaan. De database systemen die dit doen worden object-georiënteerde database management systemen (OODBMS'en) genoemd [Cattell91, Zdonik90]. Hier bespreken we kort een aantal aspecten van dit soort systemen. Meer informatie is te vinden in de literatuur waarbij ook op concrete produkten wordt ingegaan [Florijn93c, Bosman92a, Bosman92b].
Hoewel de naam anders doet vermoeden, gaat het bij OODBMS'en niet uitsluitend om de combinatie van een object-georiënteerde modellen met de traditionele taken van een database management systeem (DBMS). In feite gaat het om een nieuwe generatie van database-technologie die nieuwe data modellen - waaronder uitbreidingen van het relationele model - combineert met nieuwe faciliteiten in de (technische) architectuur van een DBMS (distributie, versioning, schema-evolutie, etc.).
OODBMS'en streven een aantal doelen na. Allereerst gaat het om het kunnen modelleren en opslaan van complexe data structuren, d.w.z. objecten die zijn samengesteld uit allerlei andere objecten. Omdat de meeste OODBMS'en uit de technische sfeer komen worden vaak voorbeelden uit CAD/CAM systemen of CASE omgevingen gehanteerd. Het efficiënt kunnen opslaan en raadplegen van grote hoeveelheden onderling gerelateerde objecten staat dus voorop. De meeste nieuwe database systemen ondersteunen dan ook object-identifiers en garanderen referentiële integriteit voor verwijzingen tussen objecten.
Verder wordt bij OODBMS'en veel aandacht besteed aan het bestrijden van de zogenaamde impedance mismatch. Deze term wordt gebruikt voor situaties waarin het data model van de database niet goed aansluit op het typemodel van de programmeertaal waarin de applicaties worden geschreven. Het is een bekend fenomeen in de relationele database omgeving. Immers, weinig tot geen traditionele programmeertalen bieden tabellen als basis datatype aan. Het gevolg is dat er steeds conversies plaats moeten vinden van het ene naar het andere model. Juist in technische toepassingen maken dit soort conversies de applicatie te traag.
Het bestrijden van impedance mismatch is een afweging van belangen. We zouden er bijvoorbeeld naar kunnen streven dat host-taal en database model volledig op elkaar aansluiten. De enige manier om dit echt goed op te lossen is dat we van één en dezelfde taal gebruik maken. Dus, de applicatie wordt geschreven in OO programmeertaal X, het data model van de database correspondeert met het typemodel van X, en de methodes in de database worden ook in X geschreven. Objecten kunnen in deze situatie min of meer transparant migreren van applicatie naar database en vice versa.
Er zijn verschillende OO database systemen die deze benadering hebben gekozen, en dus een bestaande object-georiënteerde programmeertaal (meestal C++) als data model en host-taal (en soms zelfs als DML) gebruiken. Toch is zo'n directe aansluiting tussen database en applicatie-taal niet altijd mogelijk, bijvoorbeeld omdat er al applicaties zijn die ook op de database moeten werken en die in een andere programmeertaal, evt. zelfs een niet object-georiënteerde taal, zijn geschreven. Het fundamentele probleem hierbij is dat de typemodellen van verschillende object-georiënteerde talen onderling niet compatibel zijn. In dat geval blijft er dus sprake van een bepaalde mate van impedance mismatch, en kunnen conversies nodig blijven. Een alternatief is natuurlijk om een eigen datamodel te maken met een eigen DML/DDL, en vervolgens afbeeldingen te maken naar verschillende host-talen. Ook deze benadering zien we terug bij verschillende produkten.
"Echte" OODBMS'en zijn nog niet zo lang beschikbaar. DIt betekent dat er nog het nodige te wensen overblijft rond standaardisatie van datamodellen en query-talen (OSQL). Het recente werk op dit terrein stemt hoopvol maar is nog niet volledig doorgesijpeld in de commerciële produkten. Verder is het programmeren van applicaties met persistente objecten beslist niet altijd eenvoudig. Het onderscheid tussen persistente en niet persistente klassen is beslist niet altijd eenvoudig te aken, en vereist de nodige aanpassingen aan bestaande applicaties.
Een alternatief scenario dat goed aansluit bij de bestaande investeringen is het opslaan van objecten in relationele databases. Hier zijn weer twee benaderingen mogelijk. De eerste is dat we standaard relationele databases nemen, en daar boven op een "schil" leggen die de instantie-variabelen van objecten opslaat in rijen van tabellen. Voor iedere (concrete) klasse introduceren we dan een tabel met de corresponderende attributen. Een andere oplossing is dat we het relationele model uitbreiden met object-georiënteerde kenmerken, zoals inheritance, stored methods, object-identifiers en nog een paar andere voorzieningen. Zo'n systeem [Kim92] kan dan SQL-compatible blijven, zolang geen gebruik van de uitbreidingen wordt gemaakt.
Het object-georiënteerde paradigma biedt een interessante basis voor het beschrijven en modelleren van de architectuur van complexe software systemen. De OO-benadering is schaalbaar: de zelfde basisbegrippen en ontwerpprincipes zijn op verschillende niveaus te gebruiken. De voordelen van object-oriëntatie - zoals aanpasbaarheid, configureerbaarheid van systemen en hergebruik van software - kunnen dan ook op de verschillende niveaus worden bereikt.
Voor het OO-modelleren van complexe systemen zijn extra ontwerptechnieken en ontwerpregels nodig waarvan in dit artikel een paar recente voorbeelden zijn besproken. Verder zijn standaardarchitecturen belangrijk. Ze bieden de basis voor raamwerken waar alleen de probleemspecifieke componenten nog hoeven te worden beschouwd. Door het introduceren van goede technologische ondersteuning kan hierdoor de ontwikkeling van complexe systemen aanmerkelijk worden versneld.