Componenten: Revolutie, evolutie, of devolutie? door Bart Huylebroeck
Promotor: Frans Arickx Serge Demeyer Medelezer: Jan Broeckhove
Abstract: Aan de hand van een aantal boeken wordt eerst gezocht naar een werkbare definitie van het begrip “component”. Daarna wordt nagegaan waarin het component-georienteerde paradigma verschilt van andere gangbare paradigma’s, zowel in theorie als in praktijk. Uiteindelijk is het besluit dat hoewel er geen duidelijke scheidingslijn bestaat, het componentgeorienteerd paradigma toch genoeg nieuwe aspecten aan programmeren toevoegt om effectief van een paradigma te spreken
0 Inhoud 1 Definitie 1.0 Analogie 1.1 Clemens Szyperski 1.2 Bertrand Meyer 1.3 Bruce Powel Douglass 1.4 Siemens Gang of Five 1.5 Andere 1.6 Kenmerken van componenten 2 Distinctie 2.1 Object-Oriented programming 2.2 Aspect-Oriented Programming [XEROX] 2.3 Patterns 2.3.1 Onstaan 2.3.2 Definitie 2.3.3 Toepassing 3 Eigenschappen en beloften 3.1 Correctness (correctheid) 3.2 Reliability (betrouwbaarheid) 3.3 Interoperability/compatibility (samenstelbaarheid) 3.4 Efficiency/performance (efficientie) 3.5 Testability/verifiability (verifieerbaarheid) 3.6 Reusability (herbruikbaarheid) 3.7 Changeability (veranderbaarheid) 3.8 User friendliness/ease of use (gebruiksvriendelijkheid) 3.9 Understandability (eenvoud) 3.10 Productivity (productiviteit) 3.11 Timeliness (tijdigheid) 3.12 Visibility (transparantie) 4 Huidige situatie 4.1 Besturingssystemen 4.2 Plug-in architecturen 4.3 Visual Basic 4.4 CORBA/OMA 4.4.1 Overerving en hergebruik 4.4.2 Versioning 4.4.3 Management van Objecten 4.4.4 Persistence 4.4.5 Interoperabiliteit 4.4.6 Huidig gebruik
4.5 COM/DCOM/COM+ 4.5.1 Overerving en hergebruik 4.5.2 Versioning 4.5.3 Management van Objecten 4.5.4 Persistence 4.5.5 Interoperabiliteit 4.5.6 Huidig gebruik 4.6 JavaBeans 4.6.1 Overerving en hergebruik 4.6.2 Versioning 4.6.3 Management van Objecten 4.6.4 Persistence 4.6.5 Interoperabiliteit 4.6.6 Huidig gebruik 5 Verlanglijstje 5.1 Taal 5.1.1 Toevoegen van keywords 5.1.2 Aspect-orientatie 5.1.3 Componenten als threads 5.2 Contract 5.2.1 Pre- en postcondities 5.2.2 Niet-functionele vereisten 5.2.3 Wettelijke aansprakelijkheid 6 Magic: the Gathering 6.1 Het spel 6.2 De software 6.3 Code hergebruik 6.4 Combineren van kaarten 6.5 Een alternatieve methode 7 Conclusie 8 Referenties
1 Definitie
Het begrip "component" bestaat al tientallen jaren, al van zodra het begrip "reuse" in gebruik kwam. Toch is het nog steeds nuttig om stil te staan bij de exacte definitie van de term. Vergelijking van verschillende auteurs en teksten zal duidelijk maken dat een "component" een zeer rekbaar begrip is, zelfs al zijn er duidelijke overeenkomsten.
1.0 Analogie Programmeurs streven ernaar, net zoals iedereen, om hun job zo eenvoudig mogelijk te maken. Daarbij lonken ze graag naar beproefde technieken in verwante beroepstakken, zoals de wereld van electronische componenten. In het ideale geval zou ”software engineering” sterk te vergelijken moeten zijn met "electrical engineering", waar een nieuw systeem bouwen dikwijls voor een groot deel bestaat uit bestaande componenten selecteren en verbinden met elkaar. Zo ook streven software ontwerpers ernaar om programmeren zoveel mogelijk te reduceren tot het combineren van bestaande software onderdelen. Daarom wordt de code die software componenten met elkaar verbindt al graag eens "wiring" genoemd. De vergelijking gaat natuurlijk nooit helemaal op. Bertrand Meyer en Szyperski zijn het eens dat software engineering verschilt van de andere engineering disciplines in een zeer fundamenteel opzicht: het onderscheid tussen ontwerp en product is veel vager. Het verschil tussen een tekening op papier, en de betonnen brug is vrij duidelijk: van het een kan je vallen, van het andere niet [SDM][SZYP, p.8]. Bij software engineering rijst dikwijls de vraag of een bepaalde keuze nu een kwestie van design of implementatie is. Dat vage onderscheid laat zich in verschillende kleine dingen voelen. Hoe dikwijls wordt een algoritme/procedure/handeling niet uitgelegd in pseudo-code? Hoe verleidelijk is het niet om zelf "in de code te duiken" om te weten te komen wat het precies doet? Een tweede punt waar de vergelijking mank zou kunnen lopen is de "wiring". Het woord zelf suggereert zeer eenvoudige verbindingen tussen de verschillende componenten, ongeveer zoals een geleidende draad wordt getrokken tussen twee poorten van electronische chips, of zoals cement de muur met de steunpilaar verbindt. In praktijk zou dat bij software wel eens tegen kunnen vallen. Sommige triviale dingen kunnen inderdaad zeer eenvoudig beschreven worden: in de Java beanbox bijvoorbeeld, is het mogelijk om een knop te "verbinden" met een animatie-bean op zeer intuitieve wijze, waarna de animatie kan gestart of gestopt worden met een druk op de knop. Zulke simpele en triviale verbindingen zullen jammer genoeg geen meerderheid uitmaken bij composities van componenten. Hoe leg je bijvoorbeeld vast dat enkele welbepaalde rijen cellen uit een
spreadsheet component integraal naar het database component moeten overgebracht worden, waar ze in een nieuwe database moet gestoken worden en bovendien enkele velden met geaggregeerde informatie (bijvoorbeeld een meetkundig gemiddelde) moeten worden gevuld? Drag-and-drop zal hier een beetje tekort schieten. Volgende delen gaan ietsje dieper in op de visies van enkele auteurs.
1.1 Clemens Szyperski Szyperski [SZYP] definieert een component als volgt: - A component is a unit of independent deployment. - A component is a unit of third-party composition. - A component has no persistent state. independent deployment Met "unit of deployment" worden verschillende dingen aangestipt. Ten eerste worden componenten in contrast gesteld met syntactische eenheden, zoals de klasse, of een enkele functie. Componenten kunnen verschillende objecten en procedures omvatten, zodat ze de grenzen kunnen overschrijden zoals die door een programmeertaal zijn vastgelegd. Toch vormt een component een duidelijke eenheid: het is een zeer duidelijk afgelijnd geheel, dat ofwel helemaal, ofwel helemaal niet wordt gebruikt. De interface naar "buiten" toe wordt zeer duidelijk vastgelegd, maar voor de rest geldt het component als een black box. Het woord "deployment" duidt op nog een tweede verschil met klassiekere "units of reuse": een component wordt niet gecompileerd, het wordt "deployed", dat wil zeggen het component wordt toegevoegd aan een draaiend systeem, op run-time. Dit wil ook zeggen dat een component niet wordt herbruikt als source-code, maar eerder in binaire vorm. Dit wordt erg benadrukt door Szyperski, en in columns [SDM] gaat hij iets dieper in op de noodzaak om binaire componenten te gebruiken, zodat ze op run-time niveau kunnen worden samengesteld. Indien noodzakelijk, kunnen zo zelfs systemen ontworpen worden met "hot-swapping". Dit is in sterk contrast met andere vormen van hergebruik, dat meestal op source-niveau gebeurt. "Independent" duidt op de sterke inkapsulatie. Om herbruikbaar te zijn in zeer verschillende omstandigheden, moet een component op zichzelf staan, en liefst niet te veel "verbindingen" hebben met die omgeving. Een kleine analogie: een levertransplantatie is veel moeilijker dan een harttransplantatie, enkel omdat de lever meer verbindingen heeft dan het hart. Bij componenten wordt er dus naar gestreefd om de "coupling" zo laag mogelijk te houden. Afhankelijkheden kunnen natuurlijk nooit helemaal vermeden worden. Het doel van componenten is immers om ze te combineren. Als een component dus op bepaalde
andere soorten componenten (services) rekent, moeten deze afhankelijkheden zo duidelijk, uniform en formeel mogelijk worden beschreven. Hierover later meer. third-party composition Dit heeft veel te maken met de droom van willekeurig combineerbare elementen, en het is dan ook het streefdoel voor component-georienteerde ontwikkeling. Componenten worden dan producten, die verkocht worden aan andere ontwikkelaars, die ze dan kunnen inzetten in hun software oplossing. Deze "derde partij" moet niet noodzakelijk een ander bedrijf zijn; het kan gaan om een ander departement binnen hetzelfde bedrijf, of zelfs gewoon een andere programmeur aan hetzelfde project. Bij elk software project rijst de vraag of een bepaald stuk intern moet worden ontwikkeld, of dat er bestaande software wordt gekocht. Interne ontwikkeling heeft als voordeel dat de software zeer sterk zal aangepast zijn aan de specifieke behoeften van het bedrijf, maar het brengt een vrij hoge ontwikkelingskost met zich mee, en ook een aanzienlijk risico, zowel financieel als op gebied van scheduling. Bovendien is er altijd de kans dat het warm water opnieuw wordt uitgevonden of dat de behoeften verkeerd werden ingeschat of ondertussen al geevolueerd zijn. Het kopen van bestaande software heeft als grote voordeel dat ze veel vlugger beschikbaar is, dat de aankoopprijs bekend is, en dat het onderhoud dikwijls wordt overgelaten aan de oorspronkelijke ontwikkelaar van de software. Nadeel is dat de software zelden voor 100% aan de wensen zal voldoen. En het wordt er ook niet makkelijker op om een voorsprong op de concurrenten te behouden, als de werking van het bedrijf afhangt van een product dat vrij verkrijgbaar is op de markt. persistent state Szyperski benadrukt ten stelligste dat een component geen "state" mag hebben [SZYP][SDM]. De redenering is dat als componenten zich in een bepaalde toestand kunnen bevinden, dan kunnen ze niet vrijelijk worden vervangen, of in een nieuw systeem worden ingebouwd. Het vervangende object kan niet garanderen dat het zich in dezelfde toestand bevindt, en zal dus verschillend reageren. Dit klonk me allemaal nogal verwarrend in de oren, en ik ben dus verder beginnen graven naar wat Szyperski hiermee juist bedoelt. Globale variabelen worden taboe verklaard, samen met elke constructie die globale variabelen vervangt, zoals niet-constante variabelen met class-scope. Dit is geen nieuw of radicaal standpunt: globale variabelen worden beschouwd als een bron van onvoorziene indirecte koppelingen, en ze bemoeilijken dus de documentatie, debugging en inzetbaarheid van componenten. Geen globale variabelen dus, maar wat dan met interne variabelen? Het moet toch mogelijk zijn om bepaalde gegevens bij te houden, al was het nog maar het opschrift van een simpele knop? Szyperski benadrukt dat het component niet mag verward worden met het object, en dat alle "state" in objecten moet worden bijgehouden. Een database component, bijvoorbeeld, mag niet verward worden met de eigenlijke gegevens, opgeslagen in een database met een zekere naam. Een filesystem component bevat zelf geen enkele informatie over een
toestand. In plaats daarvan voorziet het bewerkingen op objecten: de files. In theorie is het dus perfect mogelijk om het component filesysteem te vervangen in een run-time systeem. En hoewel een "delete" op een file heel anders kan geïmplementeerd in het nieuwe systeem, is de semantiek volledig dezelfde gebleven, en ondervinden de andere componenten in principe geen hinder van de hot swap. Alles komt dus neer op een tamelijk subtiel verschil in definitie van de term "component". Voor Szyperski staat een component gelijk aan een pakket van diensten. Vandaar ook zijn opmerking dat het, binnen een enkel software systeem, geen zin heeft om meer dan een component van hetzelfde type te installeren. Mijn eerste reactie daarop was dat de gemiddelde applicatie waarschijnlijk meer dan een knop zou gebruiken. Binnen Szyperski's filosofie zou een enkele knop eerder een object zijn, als onderdeel van het component "GUI". Dit staat haaks op bijvoorbeeld de JavaBeans definitie van een component, maar het leunt dichter aan tegen het COM model.
1.2 Bertrand Meyer In een reeks columns in dialoog met Szyperski [SDM], en vooral in "What to compose" van maart 2000 beschrijft Meyer wat volgens hem de kenmerken zijn van componenten. Veel van deze ideeën zijn al terug te vinden in zijn boek over object-oriented methoden [OOSC], maar het interessante aan de reeks columns is dat ze hier specifiek gebruikt worden om componenten te definiëren, en bovendien zijn ze geformuleerd in antwoord op Szyperski's definitie. "May be used by other software elements (clients)." De voornaamste bestaansreden voor componenten is gebruikt te worden door andere componenten. Samenstelling van componenten levert een totaal-systeem op dat een of meer taken kan uitvoeren, al of niet in interactie met de gebruiker. De vereiste dat componenten bruikbaar moeten zijn door andere stukken software, in plaats van de eindgebruiker legt bepaalde vereisten op in verband met de documentatie, de interface, en andere. Deze vereisten worden in de andere punten opgesomd. "May be used by clients without the intervention of the component's developers." Hergebruik is een van de belangrijkste beloften van component-georienteerd programmeren. Maar als de programmeur van het oorspronkelijke component aanwezig moet zijn om alles in goede banen te laten verlopen (bijvoorbeeld enkele aanpassingen van het component naar wens van de nieuwe klant), dan is natuurlijk alle voordeel verdwenen. Het idee achter componenten is dat een programmeur, eens alle functionele vereisten vastliggen, in een virtueel warenhuis langs de rekken zou moeten kunnen wandelen en de componenten selecteren die hem van pas kunnen komen. Daarna smeedt hij deze
tezamen met eigen code, en eventueel voegt hij nog enkele componenten van eigen makelei toe. Normaalgesproken kent de programmeur die de componenten in het warenhuis is gaan kopen de ontwikkelaar van de componenten dus niet, en toch moeten de componenten kunnen aangepast worden aan deze specifieke nieuwe situatie. Deze twee waren volgens Meyer de belangrijkste kenmerken voor componenten: hergebruik door andere software constructies, en hergebruik in een situatie waar de oorspronkelijke programmeur niet noodzakelijk bekend (of bereikbaar) is. Om deze twee mogelijk te maken, komen weer een aantal vereisten op de voorgrond, die Meyer in de volgende vijf puntjes heeft gegoten. "Includes a specification of all dependencies (hardware and software platform, versions, other components)." Om een component succesvol in te bouwen in een systeem, moet je weten wat dit component nodig heeft om te kunnen werken. Een component kan een bepaald stuk hardware nodig hebben, of het kan misschien alleen draaien onder een bepaald operating system. Maar het kan ook zeer goed zijn dat een component bepaalde andere componenten nodig heeft om te werken. Meestal wordt gezegd dat een component bepaalde diensten ("services") nodig heeft. Zo'n services moeten zeer nauwkeurig omschreven worden, zodat een development tool op een algoritmische manier kan bepalen of een bepaald component succesvol zal draaien binnen het ontworpen systeem. Als component A bijvoorbeeld een component nodig heeft dat de vuile was doet, dan moet JBuilder zeker weten dat component A de vuile was aan component B kan geven, en dat component B het gewassen en gestreken goed terug aan A zal bezorgen. De eis dat alle benodigde diensten nauwkeurig worden beschreven, heeft een logische tegenhanger: "Includes a precise specification of the functionalities it offers." Als JBuilder zeker wil weten dat alle vuile was van component A netjes zal behandeld worden, dan moet JBuilder erop kunnen vertrouwen dat component B alles zal doen. Gevolg: de diensten die component B aanbiedt, moeten nauwkeurig beschreven worden, zodat ze kunnen vergeleken worden met de diensten die component A vereist. Alleen als zowel de benodigde als geleverde functionaliteit formeel wordt beschreven, kunnen componenten gecombineerd worden volgens een bepaalde hiërarchie (een boom of een netwerk) van "A gebruikt B gebruikt C". Als de specificaties machinaal kunnen behandeld worden, kan een tool nagaan welke diensten nog ontbreken, en eventueel zelfstandig op zoek gaan naar een component dat deze dienst aanbiedt in ons virtueel warenhuis. "Is usable on the sole basis of that specification."
Als tools moeten zorgen voor compositie van componenten, dan staat of valt hun werking met de correctheid van de specificaties. Als component B van daarjuist plechtig belooft de vuile was te zullen doen, maar in plaats daarvan alle kledingstukken groen verft, dan zal het samengestelde systeem in de problemen komen. Tools moeten erop kunnen vertrouwen dat de formele specificatie ook werkelijk de semantiek van het component weergeeft. Het is onmogelijk voor een tool om eigenhandig het gedrag van een component te observeren en daaruit algemene conclusies te trekken over het toepassingsgebied van het component. Hoe kan de JBuilder zeker weten dat de kleren NOOIT kanariegeel zullen terugkomen? De dualiteit tussen specificatie en implementatie is een veelbesproken onderwerp, en ook Betrand Meyer besteedt hier de nodige aandacht aan [SDM][OOSC]. Specificaties, of interfaces zijn belangrijk om twee redenen: herbruikbaarheid ("reusability"), en vervangbaarheid ("substitutability"). Het idee van hergebruik is dat reeds software wordt ingeschakeld, zonder dat hiervoor noodzakelijk alle code opnieuw doorlopen wordt zodat een tijdsbesparing wordt gerealiseerd. Om het code-browsen te vermijden, is een specificatie nodig. Dit kan gaan van een informeel stuk tekst ("dit component kan uw was doen"), tot specifieke codes die door tools kunnen gelezen en geïnterpreteerd worden. Vervangbaarheid is het omgekeerde van hergebruik: in een bestaand systeem wordt een component verwijderd, en in plaats daarvan wordt een ander component geïnstalleerd met (op zijn minst) dezelfde interface ("wast witter dan wit!"). Dit kan alleen als alle andere componenten enkel kijken naar de interface, en niet naar de implementatie. Software engineering is vrij uniek ten opzichte van de andere ingenieursdisciplines, omdat het onderscheid tussen ontwerp en implementatie veel vager is dan in andere takken. Om het in de woorden van Bertrand Meyer te zeggen: "You can fall from the bridge, but not from the map, and you won't get an electric shock from the circuit diagram" [SDM]. De vereiste dat een component bruikbaar is enkel op basis van de specificatie lijkt dus triviaal voor andere ingenieursdisciplines, maar is dat niet altijd voor software. Heel dikwijls is het verleidelijk om documentatie te verwaarlozen uit tijdsgebrek of nonchalance, en even dikwijls is het ook verleidelijk (of zelfs noodzakelijk) om de werking van routines te controleren door de code na te gaan. Bij component-georienteerd programmeren is black box reuse dus niet alleen aangemoedigd, het is ook verplicht. En daarom is Bertrand Meyer zo blij dat componenten typisch gezien in binaire vorm worden verspreid: het is veel moeilijker om de exacte werking van een binair component te achterhalen [SDM]. Op die manier wordt "information hiding" (of "data encapsulation") afgedwongen op een manier die veel strikter is dan de meeste compilers kunnen garanderen. Omdat de source niet verkrijgbaar is, is het onmogelijk te code te inspecteren en kan dus enkel vertrouwd worden op de specificatie. En zelfs als de code toch beschikbaar is, vanuit de open-source filosofie, is het niet zo vanzelfsprekend om "even vlug" iets aan te passen in het component, omdat dat hier nu juist beter uitkomt. Szyperski werpt hiertegen op dat het onderscheid tussen binaire en leesbare vorm niet altijd even strikt is (denk maar aan scripts), en dat "data encapsulation" meestal niet de voornaamste reden is voor de binaire vorm van componenten, maar toch denk ik persoonlijk dat Meyer hier een goed argument heeft. Sterker nog: uit commerciële
overwegingen zou "information hiding" toch wel eens de belangrijkste drijfveer kunnen zijn voor de binaire vorm. "Is composable with other components." Bent u verrast? Het woord zelf suggereert dat componenten deel zijn van een groter geheel. Toch is de vereiste niet zo vanzelfsprekend als ze eerst lijkt. Zoals de voorgaande kenmerken duidelijk maken, komt er heel wat bij kijken om zinvolle combinaties van componenten mogelijk te maken. Meyer vernoemt het zelf pas in een andere column [SDM], maar hier komt ook het vraagstuk van de standaards bij kijken. "Wiring standards" worden ze genoemd, en ze zijn nodig om verschillende componenten gegevens te laten uitwisselen. Zowel Meyer als Szyperski zijn het erover eens [SZYP][SDM] dat teveel standaards het leven nodeloos ingewikkeld zouden maken, maar dat het langs de andere kant niet wenselijk is om een enkele standaard te hebben. Er zijn enkele gevaren verbonden aan pluggen die passen in apparaten die daarvoor niet bedoeld zijn, zoals Meyer duidelijk maakt in een klaagzang over zijn batterijlader die in rook opging [SDM]. Er is nog een ander aspect aan verbonden: Szyperski beschrijft hoe veel objectgeorienteerde frameworks impliciet aannemen dat ze het enige systeem zijn, en dus moeilijk gecombineerd kunnen worden met andere frameworks. Dit maakt duidelijk dat programmeren met hergebruik in het achterhoofd niet noodzakelijk hetzelfde is als programmeren met combineerbaarheid in het achterhoofd. "Can be integrated into a system quickly and smoothly." Hoe makkelijker, hoe liever. Aangezien de vorige zes kenmerken allemaal de eenvoudige integratie van componenten tot doel hebben, verdenk ik Meyer er hier van een beetje breedsprakig geworden te zijn. In de column in kwestie [SDM] stond niet veel extra uitleg bij dit punt, maar ik vermoed dat Meyer hier duidt op het feit dat een component typisch gezien ook code bevat die specifiek is voorzien om het integratie-proces gemakkelijker te maken. Ik denk hierbij aan run-time checks op de parameters, op de interne consistentie, op routines om hotswapping mogelijk te maken, etc. In "klassieke" hergebruikte code vind je zo'n run-time checks niet vaak terug, omdat samenstelling meestal gebeurt op compile-time, en de compiler dus de meeste checks uitvoert.
1.3 Bruce Powel Douglass Bruce Powel Douglass was een van de auteurs die gevangen zat in het spervuur van columns tussen Bertrand Meyer en Clemens Szyperski [SDM]. Hij heeft zelf enkele
columns geschreven, waarvan sommige een antwoord op de vragen die Meyer en Szyperski elkaar en zichzelf stelden. Douglass' definitie van componenten samenvatten is makkelijk, want hij doet het voor ons [SDM]: "A component is really a large-scale object used in an idiomatic way." Hij gaat verder met verklaren dat een component kan bestaan uit zowat elke grootteorde. Typisch gezien is een component kleiner dan een subsysteem van de architectuur, en typisch omvat het een of meerdere objecten. Doordat Douglass een component identificeert met een enkele interface, suggereert hij nochtans dat een component slechts bestaat uit een object, en ook elders gaat hij van deze veronderstelling uit. Zo'n object moet dan wel aan bepaalde vereisten voldoen om een component te kunnen zijn. Het voornaamste doel van deze eisen is de vervangbaarheid van een component binnen een bestaand systeem te vergemakkelijken. Merk op dat hij hiermee voorbijgaat aan een tweede aspect, namelijk de samenstelling van componenten tot een nieuw systeem. Douglass beperkt zich tot twee brede eisen: ten eerste moeten interface en implementatie duidelijk gescheiden zijn, en ten tweede moet het component grondig gedocumenteerd zijn. Documentatie moet zowel de interface als de pre en postcondities en varianten omvatten, het moet de aanvaardbare sequenties van functie aanroepen (het protocol) vastleggen, en ook het "gedrag". Protocol en gedrag kunnen onder andere vastgelegd worden door een "state diagram". In scherpe tegenstelling tot Szyperski, laat Douglass expliciet toe dat components ook "state" bevatten. Hun gedrag is dus niet alleen afhankelijk van de huidige parameters, maar ook van alle vorige function calls en hun parameters. Het voorbeeld dat hij aanhaalt omvat enkele componenten van hetzelfde type, namelijk twee verkeerslichten. Ook dit is in strijd met wat Szyperski eerder aanhaalde als definitie van een component. Douglass is zich hiervan bewust, maar gaat jammer genoeg niet dieper in op de reden hiervoor. Een polemiek over "statefulness" van componenten, hoewel nogal detaillistisch, had nochtans wel licht kunnen werpen op de verschillende impliciete stellingnames van de auteurs. Het ziet ernaar uit dat Douglass -samen met Meyer- componenten eerder ziet als geïnstantieerde objecten met een identiteit, dichter bij de definitie van CORBA componenten dan COM componenten.
1.4 Siemens Gang of Five
Genoemde desperado's zijn eerder berucht om hun werk op het gebied van patterns, maar in hun boek [SGOF] op pagina 385 geven ze ook een expliciete definitie van een component: “A component is an encapsulated part of a software system. A component has an
interface. Components serve as the building blocks for the structure of a system. At a programming-language level, components may be represented as modules, classes, objects or a set of related functions.” Zoals ze zelf verder ook aangeven, hebben ze het woord component heel ruim opgevat. De definitie gaat voorbij aan aspecten zoals het doel van component-georienteerde ontwikkeling, het geeft niet aan waarom een component geïnkapsuleerd is, en het legt niet meteen nadruk op het belang van scheiding tussen interface en implementatie. Aspecten zoals hergebruik, combineerbaarheid, semantiek en dergelijke worden volledig terzijde gelaten. Hoewel de definitie niet veel zegt over "good practice" in verband met componenten, komen toch veel aspecten van hergebruik, inkapsulatie, vervangbaarheid en evolueerbaarheid en dergelijke aan bod in het book. Niet onlogisch, aangezien patterns een poging zijn om "good practice" op een heldere manier op te slaan en door te geven. Veel van de patterns die betrekking hebben op objecten kunnen integraal worden toegepast op componenten. Het "observer" patroon, bijvoorbeeld, wordt intensief gebruikt bij JavaBeans en in de Beanbox, waar event-handling altijd gebeurt via het toevoegen van "listeners". Hierover later nog meer.
1.5 Andere Veel boeken die software ontwikkelingstechnieken bespreken, of die verwijzen naar hergebruik in het algemeen, zullen vroeg of laat de term "component" gebruiken. Het woord heeft dan niet noodzakelijk alle extra betekenissen die het in andere werken of deze thesis krijgt. "Component" is immers geen nieuw uitgevonden term, en dikwijls wordt het gebruikt in de klassieke betekenis van onderdeel van een compositie. Ivar Jacobson, Martin Griss en Patrik Jonsson vermelden in hun boek [IMP] op pagina 85 expliciet dat ze het woord gebruiken in de breedst mogelijke zin: A component is a type, class or any other workproduct that has been specifically engineered to be reusable. Aangezien hun boek specifiek over hergebruik gaat, ligt de klemtoon bij componenten meer bij hergebruik dan bij de andere aspecten, zoals vervangbaarheid, run-time combineerbaarheid en dergelijke. Ook Carlo Ghezzi, Mehdi Jazayeri en Dino Mandrioli staan nauwelijks stil bij het woord "component". Het boek beschouwt modularisatie technieken in het algemeen als een manier om een groot probleem onder te verdelen in verschillende kleinere problemen, die elk eenvoudiger op te lossen zijn en/of recursief onder te verdelen. Een goede module (of
component) vertoont natuurlijk wel eigenschappen zoals "data encapsulation", functionele cohesie, losse "coupling" met andere modules, enzovoort [FUND]. Dit toont aan dat veel aspecten van component-georienteerd programmeren weinig revolutionair zijn, maar eerder goede algemene principes die al zeer lang gekend zijn.
1.6 Kenmerken van componenten Gebaseerd op de auteurs die hiervoor besproken zijn, en op enkele huidige standaards voor component-systemen, ben ik tot een eigen definitie gekomen van componenten: Een component is een softwarematige eenheid, dikwijls binair, waarbij speciale inspanning is geleverd op vlak van interfaces, documentatie en code met als voornaamste doel samenstelbaarheid, herbruikbaarheid en vervangbaarheid. Zoals elke zichzelf respecterende definitie vergt ze enige toelichting. Een component wordt gedefinieerd als een eenheid. Dit betekent dat, zelfs al is ze samengesteld, naar buiten toe fungeert het als een enkel ... component. Er is geen onderverdeling mogelijk, het is niet mogelijk een component deels te herbruiken of enkele stukken aan te passen aan nieuwe behoeften. Dat componenten dikwijls binair worden verspreid is om deze eenheid nog strikter op te leggen. De interfaces zijn een formele beschrijving van de aangeboden en de vereiste functionaliteit. Het is een informatiestroom van het ene software-tool naar het andere. Componenten zoeken en samenstellen moet op basis van de interface kunnen gebeuren, en het moet grotendeels automatisch kunnen gebeuren. Een sterke scheiding tussen interface en implementatie komt ten goede aan zowel samenstelbaarheid, herbruikbaarheid als vervangbaarheid. Documentatie beschrijft het component op een minder formeel niveau. Het is informatie van de component-ontwikkelaar naar de component-gebruiker, die typisch ook een programmeur is. Documentatie dient om samenstelbaarheid en herbruikbaarheid te vergroten. Inspanningen op vlak van de code kunnen beschouwd worden als een informatiestroom van het component naar de omringende componenten. Ze beinvloeden rechtstreeks de samenstelbaarheid, herbruikbaarheid en vervangbaarheid van het component. Extra code om de binnenkomende parameters te controleren vergroot bijvoorbeeld de samenstelbaarheid. Extra code die rekening houdt met zoveel mogelijk verschillende situaties vergroot de herbruikbaarheid. Extra abstractie-lagen en strikte scheiding van specificatie en implementatie vergroten de vervangbaarheid. Samenstelbaarheid is een essentieel kenmerk van componenten: het laat toe de functionaliteit van verschillende componenten te combineren om uiteindelijk een software
systeem te bekomen dat voldoet aan gestelde eisen. Een van de constructies om samenstelbaarheid te vergroten zijn de "wiring standards" zoals COM, CORBA en JavaBeans. Een zeer sterke vorm van samenstelbaarheid is samenstelbaarheid op runtime. Voor Szyperski is dit zelfs een noodzakelijke voorwaarde om van een component te spreken. Herbruikbaarheid is de droom van elke "software engineer": door bestaande componenten in te voegen, is het mogelijk een software systeem te realiseren met veel minder tijd en middelen dan nodig zouden zijn om alles van nul te ontwikkelen. Als de herbruikbaarheid van een component optimaal is, moet de gebruiker van een component niet noodzakelijkerwijs in contact staan met de oorspronkelijke ontwikkelaar van het component. Meyer's vereiste dat een component hergebruikt moet worden zonder tussenkomst van de oorspronkelijke ontwikkelaar zit dus eerder impliciet vervat in de definitie. Vervangbaarheid komt ten goede aan wat Szyperski "evolvability" noemt: als een component kan vervangen worden door een nieuw component dat dezelfde interface realiseert, kan het systeem stap voor stap evolueren zonder dat er grote, algemene upgrades moeten gebeuren die tijdelijk het volledige systeem lamleggen en die een bedrijf dus extra tijd en geld kosten. Indien het systeem erop voorzien is, kan een bepaald component zelfs bij run-time vervangen worden, zonder enig tijdverlies. Een ander voordeel van vervangbaarheid is de mogelijkheid tot eenvoudige invoeging van een component waarvan een software fout is verbeterd, of waarvan de niet-functionele eigenschappen (zoals systeemvereisten of snelheid) zijn verbeterd. Enkele algemene principes die de vervangbaarheid ten goede komen zijn een sterke scheiding van specificatie en implementatie, en ook een lage "coupling" tussen de verschillende componenten. Nog een laatste opmerking: deze definitie gaat de discussie over "statefulness" uit de weg, om twee redenen. Ten eerste is het vermijden van "statefulness" (onder de vorm van globale variabelen, enz) volgens mij geen noodzakelijke voorwaarde om een component te maken. Het is natuurlijk wel een heel goed idee om dergelijke addertjes te vermijden, en dus valt het vermijden van "statefulness" onder de noemer "speciale inspanning met als doel …" enzovoort. Ten tweede geloof ik dat Szyperski's definitie van een component niet wezenlijk van andere definities. Een stuk software dat component-georienteerd is volgens de definitie van Meyer, zal ook component-georienteerd zijn volgens Szyperski. Alleen zal Meyer een aantal objecten aanwijzen en zeggen: "dit zijn verschillende componenten die hetzelfde gedrag vertonen, en dus tot dezelfde klasse kunnen gerekend worden", terwijl Szyperski dezelfde objecten zal aanwijzen en zeggen: "dit zijn verschillende manifestaties van hetzelfde component".
2 Distinctie
Bij elke nieuwe methodiek, hype of nieuw paradigma, is het belangrijk om zich af te vragen of het niet gaat om oude wijn in nieuwe flessen. In dit hoofdstuk wordt componentgeorienteerd programmeren vergeleken met enkele andere paradigma's.
2.1 Object-Oriented programming Wat bieden componenten meer dan objecten? De twee worden wel eens door elkaar gebruikt, en om alles nog makkelijker te maken is het onderscheid tussen klassen en objecten ook niet altijd even strikt. Een anekdote als voorbeeld van verwarring tussen objecten en klassen. In de tweede kandidatuur moesten we ("wij", dat waren toen "the RUCA Lions". Bescheidenheid is voor kwezels!) een object-georienteerd programma ontwerpen dat het virtueel geheugen in een computer nabootste. Na veel vijven en zessen hadden we een prachtig UML schema: "de processor" genereert een logisch adres, en geeft het door aan de MMU. De MMU vertaalt dit naar een virtueel adres. Dit adres kan ofwel gecached zijn, ofwel verwijzen naar een fysiek adres in het RAM geheugen, ofwel naar een adres of de harddisk, zodat het moet worden ingeswapped. De MMU sprak dus cache, RAM of disk aan, en gaf (eventueel na een swap) de waarde van het gevraagde adres terug aan de CPU. In ons software model zaten dus enkele vereenvoudigingen (zoals MMU die zelf de harddisk aansprak), maar al bij al vonden we dat we een "clean" model hadden weten op te bouwen. Nu was een van de opdrachten voor het project dat we de gebruikte strategieën moesten kunnen vervangen. Het moest dus mogelijk zijn een nieuwe MMU of een nieuwe cache te installeren, elk met specifieke vervangingsstrategieën. Zowel de cache als het RAM geheugen moesten van grootte kunnen veranderen, om de weerslag te kunnen meten op het swap gedrag van onze virtuele virtuele (sic) machine. De problemen begonnen pas bij het implementeren. De MMU bijvoorbeeld was een object dat bijna alle ander objecten moest kunnen aanspreken, en omgekeerd. Maar tegelijk moesten vele objecten ook kunnen vervangen worden terwijl het programma liep. In onze prachtige schema's liepen er simpelweg een aantal pijlen van MMU naar CPU, van MMU naar cache, van MMU naar RAM. In praktijk bleken er verschillende kandidaat-MMU's te zijn, en verschillende kandidaat-caches, enzoverder. We hebben alles dan opgelost door middel van stapels en stapels pointers, en ontwierpen we een heel stel ingewikkelde procedures om ervoor te zorgen dat als bijvoorbeeld de cache werd vervangen, dat dan alle pointers naar de huidige cache ook vervangen werden.
Wat was er gebeurd? Het antwoord op deze vraag kwam pas bij me op terwijl ik deze thesis aan het voorbereiden was: we hadden het prototype verward met de eigelijke objecten. In ons model sprak het prototype van de MMU het prototype van het RAM geheugen aan. We stonden niet stil bij de vraag hoe de MMU het RAM geheugen zou vinden. Dat leek ons even vanzelfsprekend als de kwestie hoe het MMU de klassedefinitie van het RAM geheugen zou weten te vinden. We hadden het onbewuste idee dat als we de headerfile van de RAM-klasse toevoegden aan de code van de MMU, dan liet de compiler ons toe om het RAM aan te spreken. Maar weten dat een bepaalde klasse bestaat is nog niet genoeg om ook effectief objecten van die klasse terug te vinden. Onze ontwerpen hielden dus geen rekening met het onderscheid tussen klasse en object. Bij componenten is het onderscheid tussen klasse en object, tussen specificatie en implementatie, zo mogelijk nog belangrijker. Ik zou willen beginnen met te zoeken naar een goede definitie van wat objecten juist zijn, en van daaruit te vertrekken om duidelijk te maken dat componenten een meerwaarde bieden.
2.1.1 Object-Oriented Programming: a Unified Foundation [OOPUF] Dit boek biedt een formele notatie om eigenschappen van object-georienteerde talen te beschrijven. Het benadert object-orientatie vanuit een sterk taal-theoretische hoek, en minder vanuit de "software engineering" hoek. De potentiële voordelen van componenten liggen echter niet in het taal-theoretische. Component-georienteerd programmeren wordt niet geassocieerd met een volledig nieuwe programmeertaal, maar vertrekt vanuit bestaande talen zoals C++, Java, VB, Eiffel, en andere. Maar het boek kan misschien wel dienen om een goede definitie van object-orientatie te geven. Op pagina 38 staat: “Objects are programming items grouped in classes and possessing an internal state that may be accessed and modified by sending messages to the object.” Iets verder, op pagina 40 staat: “An object is a programming unit that associates data with the operations that can use or affect these data.” Een object is dus, net als een component, een eenheid. Dit suggereert dat objecten geschikte constructies kunnen zijn om componenten van te bouwen. Het definieert een aantal operaties. Dit is gelijkaardig met de interfaces van componenten, maar in deze definitie is met opzet in het ongewisse gelaten hoe de definitie van operaties juist gebeurt. Merk ook op dat de eerste definitie expliciet gewag maakt van een "internal state", en dus tot op zekere hoogte onverenigbaar is met de definitie van componenten die Szyperski geeft.
Giuseppe Castagna beschouwt "late binding" als de belangrijkste eigenschap van objectorientatie: als een member-functie van een object wordt opgeroepen, is het op compiletime niet meer noodzakelijk bepaald welke code precies zal worden uitgevoerd. Als "vorm" een interface is die de methode "draw()" bevat, kunnen het object "vierkant" en het object "cirkel" (beide subtypes van "vorm") deze methode een heel andere invulling geven. Als ons tekenprogramma dan een "vorm" moet tekenen, roept het simpelweg de methode "draw" op, zonder te moeten nagaan of de vorm in kwestie nu een vierkant, cirkel, of iets geheel nieuw is. Terzijde: Castagna brengt late binding terug tot een vorm van function overloading, waarbij hij aantoont dat typische object-georienteerde talen een zekere vorm van function overloading negeren, omdat ze uitgaan van minimale informatie bij het compileren. Dat is eenvoudig aan te tonen met twee functies die dezelfde naam hebben, maar waarbij de een een argument van het basistype aanvaardt, terwijl de andere een argument van het subtype aanvaardt. De voorbeeld-code toont aan dat onder bepaalde omstandigheden de functie wordt aangeroepen die het basistype behandelt, terwijl toch een subtype als argument werd gegeven. Om terug te komen op de relatie tussen objecten en componenten: de "late binding" bevordert vervangbaarheid doordat een subtype kan gebruikt worden waar een basis type werd verwacht, en het bevordert samenstelbaarheid doordat nieuwe subtypes kunnen gebruikt worden door code die geschreven kan zijn nog voor het subtype bekend was. Het mag duidelijk zijn dat "late binding" een belangrijk hulpmiddel is bij zowel object-orientatie als bij componenten. Szyperski introduceert nog een hulpmiddel voor componenten: "very late binding" [SZYP].
2.1.2 A Theory of Objects [THEO] Het boek opent met een definitie van objecten op pagina 8: “The Object-Oriented approach to programming is based on an intuitive correspondence between a software simulation of a physical system and the physical system itself. An analogy is drawn between building an algorithmic model of a physical system from software components and building a mechanical model of a physical system from concrete objects. By analogy, the software components are themselves called objects.” Merk op dat het woord "components" hier wordt gebruikt in de meest algemene betekenis: onderdeel van een samenstelling. Voor de rest benadrukt de definitie een overeenkomst tussen de objecten binnen de software, en de objecten in de wereld die door de software gemodelleerd wordt. Hier schuilt zowel de sterkte als het gevaar bij object-georienteerde analyse: object is zowat het meest vage woord dat er bestaat in het vocabularium van een
taal, en het kan werkelijk op alles slaan. Het boek ontleedt de grote en vage groep van objecten in drie delen, die elk geassocieerd worden met een fase binnen een objectgeorienteerd project. In de analyse-fase zijn objecten concrete dingen die gemodelleerd zullen worden. Als bijvoorbeeld een softwarepakket wordt ontworpen om een zonnestelsel uit te beelden zullen de objecten planeten zijn. In de designfase komt er een soort objecten bij: ze zijn geconcretiseerde abstracties, nodig om het model te beschrijven. Planeten, bijvoorbeeld, hebben elk hun eigen omloopbaan. Dit zijn geen fysieke entiteiten meer, maar wel degelijk "dingen" (let op de vage term) om het model te laten werken. Uiteindelijk resulteert dit alles in software-matige implementaties, in code die onderverdeeld is in objecten. Sommige daarvan zijn de implementatie van de fysieke objecten (planeten), andere van de abstracties (omloopbanen), en nog andere zijn alleen gecreëerd om technische implementatie-redenen (lijsten, abstracte datatypes, ...) Dit alleen maar om mijn punt te onderstrepen dat component-georienteerd programmeren in hetzelfde bedje ziek is. Het woord "component" is net zo vaag als "object", en net zo vatbaar voor misverstanden. Ook componenten zullen gebaseerd zijn op een intuïtieve overeenkomst tussen een software-model en de fysieke wereld waar de software betrekking op heeft. Zo zullen er componenten zijn die betrekking hebben op fysieke entiteiten. In het project dat deze thesis vergezelt zitten bijvoorbeeld componenten die personen voorstellen, of kaarten uit het spel. Daarnaast bestaan er componenten die bepaalde abstracties voorstellen. In mijn project zitten componenten die "schade" voorstellen, of een component dat het spelverloop voorstelt. Dat component vervangen zou betekenen dat andere spelregels gelden. En tenslotte bestaan er componenten die gecreëerd zijn enkel omdat ze de creatie van software vergemakkelijken. Een database component bijvoorbeeld stelt niks uit de fysieke wereld voor, maar maakt het wel een stuk makkelijker om bepaalde gegevens op te slaan en terug te vinden. In deze zin vertoont component-georienteerd ontwerpen dus veel overeenkomsten met object-georienteerd ontwerpen. Ik denk dat het voornaamste verschil er een van schaal is: in een component-georienteerd ontwerp zal een component typisch gezien groter zijn en/of een belangrijker deel uit de werkelijkheid modelleren dan een object. Maar dit is geen wetmatigheid, en in object-georienteerde systemen kunnen zeker objecten voorkomen die groter zijn dan een flink uit te kluiten gewassen component.
2.1.3 "Het beste boek Java 2.0" Het ontbreekt dit boek nochtans niet aan ambitie, maar uiteindelijk is de bedoeling van het boek het aanleren van een enkele programmeertaal: Java. Omdat Java een objectgeorienteerde taal is, waagt het boek zich ook aan een definitie van object-orientatie. Ik heb dit boek genomen als voorbeeld voor meer populaire literatuur, die programmeren niet zozeer vanuit de academisch-theoretische hoek benadert, maar vanuit een semiprofessionele hoek. Op pagina 241 staat:
“In plaats van programma's op een top-down lineaire manier aan te pakken zoals traditionele programmeertalen zoals Pascal of C, probeert OOP een probleem in onderdelen te splitsen. De oplossing concentreert zich op deze onafhankelijke objecten en hun relatie met andere objecten.” Het eerste dat me opviel was een sterke identificatie van programmeertaal met het programmeerparadigma. Dit is niet altijd onterecht: vele talen werden ontworpen om nieuwe inzichten in te verwerken, en lenen zich dus beter tot dat ene paradigma dan tot andere. Zo werd Pascal ontworpen om procedureel programmeren te vergemakkelijken. Modula-2 zag het levenslicht om nieuwe vereisten in verband met apart compileren in te vullen. Oberon om object-georienteerde concepten in te verwerken, en Eiffel kwam er om ideeen over object-orientatie en hergebruik in te verwerken. Toch is het perfect mogelijk om in Oberon of Eiffel op een procedurele manier te werken, en object-georienteerde constructies te negeren. Omgekeerd is het ook mogelijk om zelfs in assembler op een object-georienteerde manier te werk te gaan. Alleen zijn er geen speciale constructies voorhanden om alles makkelijker en minder foutgevoelig te laten verlopen. Toch is de link tussen taal en paradigma niet vergezocht. Misschien is het wel een goed idee om een taal te ontwerpen die specifiek bedoeld is om componenten in te programmeren? Hier zou ik graag op terugkomen in het verlanglijstje. De definitie stelt ook dat er een contrast is tussen object-georienteerde analyse, en de "top-down lineaire manier". Hoewel het boek daarmee doelt te suggereren dat de objectgeorienteerde manier makkelijker of natuurlijker is, duidt het wel op een mogelijk probleem bij object-georienteerd design: de niet-lineariteit van de resulterende constructie. Szyperski behandelt de mogelijke gevaren in meer detail. 2.1.4 Clemens Szyperski [SZYP] Szyperski's definitie van objecten is zeer summier: "encapsulation of state and behaviour, polymorphism, and inheritance" [SZYP]. Tegelijk zijn dit ook allemaal eigenschappen van componenten. Verder op pagina 10 wordt ook weer duidelijk dat Szyperski en andere auteurs lichtjes verschillende visies hebben op componenten, en dus hun relatie tot objecten. Szyperski stelt vast (hoewel hij geen bronnen vernoemt) dat individuele objecten bijna nooit worden verkocht of ingezet. Hij concludeert daaruit dan een component dus een meer statische eenheid is: een klasse, of een groep klassen (bv. in een module). Vandaar ook zijn stelling dat een component als geheel nooit een instantie is, in tegenstelling met bijvoorbeeld de visie van Meyer of met het idee achter Javabeans. Maar wat maakt een component dan meer dan een -of meer- klassen? Ten eerste omvat de definitie van objecten geen eisen omtrent samenstelbaarheid, en ze gaat ook voorbij aan commerciële eisen. Er is nooit een levende markt gekomen voor objecten (of klassen)
gekomen, op enkele succesvolle uitzonderingen na, en dat valt te verklaren door de catch 22 rond objecten en ook componenten: de extra inspanning die het kost om een wijd herbruikbaar component te maken in plaats van een specifieke oplossing, is alleen gerechtvaardigd als het wordt terugverdiend op de markt [SZYP]. Zo'n markt moet dus al bestaan voor ze op gang kan komen. Szyperski noemt ook nog een aantal andere oorzaken waardoor object-georienteerde technologie er mogelijks niet in is geslaagd om een bloeiende markt in objecten te starten. Een typisch object (en naar mijn mening ook een klasse; al lijkt Szyperski hier het onderscheid niet te maken) heeft geen duidelijke bedoeling voor de eindgebruiker. Het is meer een gebruiksvoorwerp van de doorgewinterde programmeur. Zelfs in het beste geval is hergebruik van objecten (en klassen) dus voorbehouden voor een relatief kleine doelgroep. Meestal zijn ze zelfs helemaal niet bedoeld voor hergebruik door een derde partij, maar hoogstens door de oorspronkelijke programmeur. Om woorden te gebruiken uit mijn eigen definitie voor componenten: meestal zijn te weinig inspanningen geleverd om samenstelbaarheid en uitbreidbaarheid te garanderen. Een andere eigenschap van object-georienteerd design is dat ze niet noodzakelijk lineair is. Terwijl een procedureel design vrij sterk gelaagd is (toepassing bovenop libraries bovenop systeemsoftware bovenop hardware), leent object-orientatie er zich toe om een veel minder gelaagd systeem op te bouwen met clusters of netwerken van objecten die elkaar kunnen aanspreken. Constructies als inheritance en overloading creëren extra verbindingen die gewoonlijk laag-overschrijdend zijn, en dus de lineariteit niet ten goede komen. Op pagina 48 en verder doet Szyperski uit de doeken welke gevaren er kunnen optreden als de top-down lineariteit wordt doorbroken. In functionele omgeving (zonder callbacks) kunnen functies alleen beroep doen op functies die in dezelfde of een lagere laag gesitueerd zijn. Deze functies kunnen op hun beurt dan weer functies oproepen uit dezelfde of onderliggende lagen. Met andere worden: controle wordt nooit aan een hogere laag teruggegeven voordat de lagere functies volledig uitgevoerd zijn en de onderliggende laag dus terug in een stabiele, consistente toestand is. De pre- en postcondities van een functie in de onderliggende laag laten geen ruimte voor glitches: voor de aanroep van de functie moet de preconditie gelden, als de functie terugkeert moet de postconditie gelden. De bovenliggende laag merkt niks van de uitvoering van de onderliggende functie, en kan dus ook geen inconsistente toestand opmerken. Vervolgens doet Szyperski uit de doeken hoe callbacks deze schijnbaar triviale vereiste kunnen doorbreken. Een callback functie is deel van een hoger-gelegen laag, maar ze wordt opgeroepen vanuit een lager deel uit de hiërarchie. Terwijl de onderliggende laag dus nog bezig is met de verwerking van een opdracht (de onderliggende functie is nog niet beeindigd), wordt de hogere laag terug actief. Op dat moment is het mogelijk dat de bovenliggende laag (de callback functie) een toestand waarneemt die niet consistent is. Dit is allemaal nogal abstract, en ik verwijs graag naar Szyperski's boek voor een meer gedetailleerd voorbeeld. Als we overgaan naar objecten, dan kan ik een voorbeeld geven dat geinspireerd is op wat Szyperski aanvoerde, maar specifiek toegepast op java. Voor
javabeans voorziet java in zogenaamde "constrained properties". Dit zijn attributen van een component die van waarde kunnen veranderen, maar waarbij de verandering kan worden tegengehouden door andere componenten. In de beanbox is er bijvoorbeeld een JellyBean component, dat van kleur kan veranderen. Een ander component, het Voter component, kan zich als registreren om hiervan op de hoogte te worden gebracht. Als het Voterbean niet akkoord is met de verandering (en dit gebeurt nogal dikwijls), dan wordt een exceptie gegenereerd, en de Jellybean keert terug naar zijn oorspronkelijke kleur. De moeilijkheden beginnen pas wanneer nog een andere Voterbean ook op de Jellybean was geregistreerd, die WEL akkoord ging met de verandering. Om te vermijden dat die bean nu denkt dat de Jellybean van kleur is veranderd, moet ze verwittigd worden dat de oorspronkelijke kleur is hersteld. Maar wat gebeurt er nu als de oorspronkelijke kleur niet naar de zin was, van geen enkele Voterbean? Juist: dan worden er weer excepties gegenereerd. Om volledig te zijn, moet ik hieraan toevoegen dat Szyperski drie soorten "inheritance" onderscheidt. "Implementation inheritance" is een vorm van code reuse: sommige functies worden opnieuw gedefinieerd in het subtype, maar andere blijven behouden uit de basistype. Deze mix vormt een bron van problemen die sterk lijken op degene die zojuist beschreven zijn met callbacks: het subtype kan controle verkrijgen terwijl het basistype niet noodzakelijk in een consistente toestand is. "Implementation inheritance" vereist een grondige kennis van de implementatie van het basistype, en valt dus moeilijk te rijmen met "black-box reuse". Dan is er ook nog "interface inheritance", waarbij een nieuw type wordt gevormd dat het volledige contract overneemt en eventueel uitbreidt van het oude type. Hier wordt dus geen code overgeërfd. En ten laatste is er nog wat Szyperski "establishment of substitutability" noemt. Het kan logisch lijken dat als een type overerft van een oud type dat het nieuw type kan ingezet worden waar het oude type werd verwacht. Dit is niet voor alle object-georienteerde talen het geval. De Smalltalk library bevat klassen die niet inzetbaar zijn waar hun basistype dat wel is, Eiffel laat toe bepaalde overgeërfde definities teniet te doen, en in C++ is het mogelijk een klasse "private" over te erven. Dit maar om te zeggen dat inheritance eigelijk verschillende technieken omvat, die elk verschillende doelen kunnen dienen. Sommige daarvan komen overeen met de doelen gesteld bij component-georienteerd programmeren: het overerven van een interface om vervangbaarheid te garanderen, bijvoorbeeld, is een basis concept voor componenten. En hoewel Szyperski "implementation inheritance" niet volledig wil afzweren, waarschuwt hij toch voor de mogelijke gevolgen. Zelf is hij een groter voorstander van "delegation": in plaats van een component met code en al over te erven, wordt dan alleen de interface overgeërfd. Taken die het nieuwe component niet zelf wenst af te handelen, worden dan gedelegeerd aan het component waarvan de interface werd overgeërfd. Op deze manier zijn de afhankelijkheden veel explicieter.
2.1.5 Bertrand Meyer In zijn boek [OOSC2] definieert Meyer object-orientatie als een methode die zowel analyse, design als implementatie omvat. Object-georienteerde principes maken het mogelijk een structuur op te leggen door onderverdeling in klasses. Ze omvat een discipline om hergebruik zo betrouwbaar mogelijk te maken via definitie van interfaces (in zijn woorden: contracten). Object-orientatie is ook een "epistemologisch" principe dat vastlegt hoe objecten alleen via hun klasse gekend zijn, en dus abstracte datatypes (zouden) moeten vormen. Tenslotte omvat het een manier van classificeren die het mogelijk maakt om externe verbanden zoals "is een" vast te leggen: overerving. Meyer begint met te definiëren wat een goede programmeertechniek, en object-orientatie in het bijzonder probeert te bereiken: hij noemt kwaliteitsaspecten zoals correctheid, robuustheid, uitbreidbaarheid, herbruikbaarheid, compatibiliteit, efficientie, portabiliteit ("portability"), eenvoud van gebruik, functionaliteit, en tijdigheid [OOSC2]. Object-georienteerde methoden proberen dus net dezelfde doelen te bereiken als component-georienteerde methoden. Sterker nog: ook procedureel programmeren probeerde al deze kwaliteiten te realiseren. En de manier waarop Meyer het objectgeorienteerd paradigma omschrijft is ook van toepassing op het component-gebaseerd paradigma. Component-georienteerd programmeren begint ook al van bij de analyse, en structuur wordt opgelegd niet via klasses, maar in componenten. Hergebruik wordt ook bevorderd door definitie van interfaces en contracten, kennisrepresentatie gebeurt ook via de methoden van een component. Zoals Meyer het uitdrukt over objecten: "what we know of something is entirely defined by how we can use it" [OOSC2]. Hetzelfde geldt ook voor componenten. Tenslotte kennen ook componenten een onderlinge relatie van overerving. Het is dan ook niet verwonderlijk dat Meyer component-georienteerd programmeren beschouwt als een logische, volgende stap in object-georienteerd programmeren. Objectgeorienteerde talen bevatten veel methoden die ook worden gebruikt voor componenten, en daarom is het volgens Meyer makkelijker om een object-georienteerd programma te converteren tot een component-georienteerd programma. Of nog; het is eenvoudiger een component te distilleren uit een goed ontworpen object, of uit een verzameling klassen. Merk op dat voor Meyer een component kan bestaan uit zowel een object, als een klasse (of meerdere van beide) [SDM]. Maar als u de zeven kenmerken van componenten bekijkt die Meyer formuleerde, dan ziet u dat deze allemaal ook kunnen gelden voor een doordacht ontworpen object (of klasse). Meyer beschouwt object-orientatie niet als een absoluut kenmerk [OOSC2]: een taal of programma kan meer of minder object-georienteerd zijn. In dezelfde zin denk ik dat component-orientatie geen absoluut kenmerk is. Objecten, zelfs als ze in de vroege dagen van object-orientatie zijn ontworpen, kunnen in hoge mate al een component zijn. Een belangrijke stap om objecten nog meer component-achtig te maken is dan het verspreiden van de code in binaire vorm, in plaats van als source-code, en het mogelijk maken om het component op run-time in een systeem bij te voegen.
Die binaire vorm heeft twee redenen, zoals duidelijk wordt in de polemiek tussen Meyer en Szyperski [SDM]. Ten eerste is het logische gevolg van een binaire vorm dat het minder makkelijk is om de precieze implementatie te achterhalen, en dus wordt zo op een natuurlijke vorm "information hiding" toegepast. Ten tweede maakt de binaire vorm het mogelijk dat een component wordt ingezet (bijna) zonder menselijke tussenkomst. Dit vergroot dus de herbruikbaarheid en de samenstelbaarheid. Nu is het wel zo dat de definitie van "binair" een beetje vaag is. Voor scripts bijvoorbeeld is de binaire vorm gelijk aan de source code. De "information hiding" van binaire componenten is dus niet altijd even effectief, terwijl je zou kunnen zeggen dat activering met een minimum aan menselijke tussenkomst eigelijk deel is van de definitie van de term "binair". 2.1.6 De verschillen op een rijtje In vergelijking met component-georienteerd programmeren gebruikt het objectgeorienteerde paradigma gelijkaardige technieken om gelijkaardige doelstellingen te verwezenlijken. Zowel Meyers definitie als mijn eigen definitie kunnen dan ook geen strikt onderscheid maken tussen objecten (of klassen) en componenten. De definitie van Szyperski zoals ze eerder werd geciteerd gebruikt het woord "deployment", en duidt op het run-time samenvoegen van componenten. Omdat voor het uitvoeren van een component binaire code is vereist, valt sourcecode van een klasse buiten Szyperski's definitie van een component. Vertrekkend vanuit de definitie gegeven in 1.6 zou ik willen besluiten dat objecten en componenten uiteindelijk alleen verschillen in de mate waarin inspanningen zijn geleverd op het vlak van interface, documentatie en code, en de mate waarin de doelstellingen gerealiseerd worden van samenstelbaarheid, herbruikbaarheid en vervangbaarheid. Op vlak van interface zullen contracten van componenten uitgebreider en strikter moeten zijn dan op dit moment mogelijk is. De IDL van CORBA en COM zijn een eerste stap, maar de signature van een functie zegt nog niks over de semantiek, en het is ook onmogelijk om een protocol van interactie vast te leggen. Documentatie kan zich richten op twee doelgroepen richten. Ten eerste is er de programmeur die het component zal gebruiken binnen zijn eigen ontwerp, en ten tweede is er de eindgebruiker, die eventueel het component rechtstreeks zal aansturen, als er een grafische interface is voorzien. De code van een component is typisch onder te verdelen in twee stukken: het deel dat de eigelijke functionaliteit voor zich neemt, en het deel dat de samenstelling van het component met andere componenten verzekert. De code die de functionaliteit verzorgt is typisch de enige soort die men in een object tegenkomt, en het behandelt dingen zoals controleren en manipuleren van parameters, en aanroepen van ondergeschikte componenten. Het deel dat de samenstelling met andere componenten verzorgt bestaat uit code die benodigde componenten zoekt, bijvoorbeeld via "object request brokers"
zoals CORBA of COM. Het kan ook persistentie mogelijk maken, of migratie van het object naar andere runtime omgevingen, computers of netwerken. In de javabeans standaard is bijvoorbeeld voorzien dat bepaalde helper-klassen alleen bestaan om de configuratie van het eigelijke bean mogelijk te maken. Deze klassen zijn dus code die bedoeld is om de samenstelling te vergemakkelijken. Samenstelbaarheid is een eigenschap van zowel objecten als componenten: bijna geen enkele niet-triviale taak wordt vervuld door een enkel object, of een enkel component. Maar het typische object wordt samengesteld op compile-time. Of, correcter: klassen worden samengesteld op compile-time, en dus staat op run-time al vast hoe de objecten zullen interageren. Dit is natuurlijk in het ideale geval: bugs bewijzen dat objecten niet altijd interageren zoals voorzien. Een component daarentegen is "meer" samenstelbaar als het ook nog bij een lopen programma kan bijgevoegd worden, bijvoorbeeld doordat de diensten ervan werden ingeroepen via een ORB. Herbruikbaarheid was de grote belofte van het object-georienteerde principe. Het is de belofte van component-georienteerd programmeren om deze herbruikbaarheid naar een hoger niveau te tillen, door een markt voor componenten mogelijk te maken dankzij de andere eigenschappen van componenten. Op die markt kunnen dan componenten aangekocht en hergebruikt worden in situaties die niet noodzakelijk door de oorspronkelijke programmeur gekend of voorzien moeten zijn. Technieken uit de object-georienteerde wereld zoals inheritance en polymorphisme zorgen dat zowel objecten als componenten kunnen worden ingezet waar oorspronkelijk alleen een basistype werd verwacht. Een extra vorm van vervangbaarheid voor componenten geldt als ze kunnen vervangen worden zonder hercompilatie, eventueel zelfs zonder het programma opnieuw op te starten. Volgens Szyperski is dit zelfs een noodzakelijke voorwaarde om van componenten te kunnen spreken [SDM].
2.2 Aspect-Oriented Programming [XEROX] Aspecten worden gedefinieerd als een nieuwe vorm van modulariteit. Ze staan dwars op de "klassieke" vormen van modulariteit, zoals functies, modules, of klasses, maar aspecten zijn wel bedoeld om gebruikt te worden tezamen met andere, meer klassieke soorten modulariteit [SKM]. In een typisch modulair systeem, opgesplitst in functies, klasses of modules, zijn er toch nog veel weerkerende elementen. Bepaalde aspecten(!) van programmeren zijn niet zo makkelijk in een enkele klasse of module te omvatten, maar duiken op doorheen het systeem, in sterk op elkaar lijkende vormen. Voorbeelden van aspecten die de modulegrenzen overschrijden zijn "error-checking", synchronisatie, "resource handling", optimisaties, logging, error-afhandeling, enzovoort. Ze zijn meestal moeilijk te vatten in
een duidelijk afgelijnde klasse, maar overal verspreid in de modules zitten codefragmenten die sterk op elkaar lijken [XEROX]. Aspect-Oriented Programming wil deze verspreide fragmenten terug bijeen brengen in een enkele taal-constructie, zodat het eenvoudiger wordt om een overzicht te behouden van wat er doorheen de modules gebeurt, en zodat het ook eenvoudiger (en minder foutgevoelig) wordt om veranderingen aan te brengen. Door deze fragmenten bijeen te brengen worden ook stukken code hergebruikt, uitgebreid, of veranderd. Een nieuw soort module is geboren, en de naam is "aspect". Een voorbeeld. AspectJ is een taal die gebaseerd is op Java, en uitgebreid met een nieuwe constructie, het aspect. Binnen zo'n aspect is het mogelijk om een "crosscut" te definieren. Een "crosscut" kan gedefinieerd worden op bepaalde modules, bepaalde methoden en/of bepaalde argumenten. Een crosscut kan bijvoorbeeld slaan op alle methoden "setX(int x)" binnen alle modules "java.*". Daarna is het mogelijk een stuk code te definieren dat wordt uitgevoerd elke keer een bepaalde "crosscut" van toepassing is. Zo kan de body bijvoorbeeld bevatten "System.out.println("X wordt op " + x + " gezet!");". [AJ] Mijn eerste idee was dat het hele aspect-idee een nogal uitgebreide constructie was met een relatief beperkt toepassingsgebied. Ik kon zeker zien dat de code voor logging en debugging makkelijker te centraliseren waren in een aspect, maar hoeveel andere "aspecten" bestonden er eigelijk? Op de website van AspectJ wordt nog een goede toepassing vernoemd: pre- en postconditie verificatie. Het is mogelijk om voor alle functies met een bepaald signatuur na te gaan of voor de aanroep bepaalde condities gelden, en of na de aanroep bepaalde andere condities gelden. Een taal als Eiffel bevat al constructies om pre- en postcondities te testen, maar het is mogelijk om aspect-georienteerde constructies te zien als een veralgemening van mechanismen om contracten te controleren. Terwijl ik aan het begeleidende project aan het programmeren was, viel het mij dikwijls op hoeveel sterk gelijkaardige code en alle klassen voorkwam. Bij javabeans heeft elk "publiek" attribuut een setter, een getter, en een event-model volgens het Observerpatroon om veranderingen van het attribuut kenbaar te maken. In totaal gaat het om 4 tot 8 methoden, voor 1 enkel attribuut. In andere klassen komen exact dezelfde methoden voor, behalve wat betreft naam en type van het attribuut. Aspect-georienteerd programmeren zou de code van al deze methoden kunnen samenbrengen in 1 enkel aspect, en niet alleen een hoop werk besparen maar de zaak ook overzichtelijker maken. En dat leidt meteen tot de conclusie: aspect-georienteerd programmeren is perfect te combineren met component-georienteerd programmeren, en vult het paradigma zelfs aan. Het kan inderdaad een vorm van moduleren zijn die dwars staat op de indeling in componenten, en kan op die manier structuur brengen in zaken zoals een uniforme errorcorrectie, parameter-checking. De vraag is of de verdiensten (en het aantal mogelijke toepassingen) van aspect-georienteerd programmeren ook groot genoeg zijn om het ontstaan van een nieuwe programmeertaal met eigen paradigma's te rechtvaardigen.
2.3 Patterns De korste definitie van patterns is dat het oplossingen zijn voor problemen in een bepaalde context [IPAL]. De oorspronkelijke term en het idee gaan terug tot Christopher Alexander [ALEX], een architect die merkte dat een zuiver modulaire manier van bouwen niet aan de verwachtingen voldeed, en dus zocht naar een aanvullende methodiek. Het idee van patterns is overgewaaid naar andere disciplines, waaronder software engineering [INTRO]. 2.3.1 Onstaan Al eeuwen geleden hebben auteurs ideeën geformuleerd die als patterns kunnen worden beschouwd, helemaal tot aan Lao Tzu [IPAL]. Patterns vonden hun weg naar de software wereld begin jaren negentig via "proto-patterns" zoals C++ idioms [IDIOM]. Ook begin jaren negentig kwamen vier individuen met elkaar in contact die gelijkaardige ideeën aan het uitbroeden waren. Dit zou resulteren in een boek [GO4], en de vier werden berucht onder de naam "Gang of Four". Vijf andere desperado's publiceerden een boek waarin patterns niet zo strikt object-georienteerd meer waren, en werden prompt berucht onder de naam "Siemens Gang of Five" [SGOF]. Nu is patterns een nieuw mode-woord, en worden internationale conferenties georganiseerd om nieuwe patterns te bespreken [PLOP]. Zoals elke nieuwe hype worden er heel wat loze beloften gedaan rond patterns, en John Vlissides, een van de Gang of Four heeft het in een artikel op zich genomen om enkele van die mythes te doorprikken [MYTH]. 2.3.2 Definitie Een van de eerste misverstanden die John Vlissides uit de weg ruimt is de enge definitie van een patterns als "a solution to a problem in a context". Deze definitie mist enkele belangrijke kenmerken van een pattern. Vooraleer ik deze kenmerken probeer op te sommen, wil ik eerst nog Brad Appleton's definitie van een pattern citeren [INTRO]: “A pattern is a named nugget of insight that conveys the essence of a proven solution to a recurring problem within a certain context amidst competing concerns.” Een pattern heeft een naam. Dit kan triviaal lijken, maar de bedoeling hiervan is om zowel probleem, context, oplossing enzoverder in een simpele term te kunnen verwoorden. Op die manier wordt een vocabularium gecreëerd dat communicatie tussen software
engineers verkort en verduidelijkt, dat dubbelzinnigheden vermijdt en zelfs in zekere zin gedachten vormgeeft. Het is een stuk makkelijk om het Observer pattern te vernoemen, dan om een stel klassen te beschrijven die via een methode van een andere klasse in een lijst worden gestoken zodat elk van deze klassen een message krijgt op de moment dat een asynchroon event wordt gegenereerd. Als u begrijpt wat ik bedoel. Een pattern geeft vorm aan inzicht en ervaring dat normaal alleen gegeven is voor ervaren programmeurs. Zij hebben gemerkt dat bepaalde vraagstukken steeds terugkomen, onafhankelijk van de designmethode of het paradigma dat gevolgd wordt. Bepaalde krachten moeten in een zo optimaal evenwicht worden gehouden, en over de jaren hebben ervaren programmeurs gemerkt dat de goede balans niet zo ver ligt van de laatste keer dat ze dit probleem tegenkwamen. Vooraleer een pattern als zodanig wordt aanvaard, moet duidelijk zijn dat de voorgestelde oplossing bruikbaar is in verschillende situaties. Over het algemeen wordt de regel van drie toegepast: als een pattern geldt in drie middelgrote tot grote toepassingen, geldt het als bewezen. Een pattern stelt dikwijls een oplossing voor die verschillende, soms conflicterende belangen verenigt. Bij het Observer pattern gelden bijvoorbeeld twee krachten: ten eerste zijn er bepaalde objecten of klassen die moeten verwittigd worden wanneer een event optreedt. Ten tweede is het altijd raadzaam om de "coupling" tussen verschillende klassen zo klein mogelijk te houden. Beide krachten zijn in conflict omdat het rechtstreeks aanroepen van een methode in een andere klasse de "coupling" vergroot. Het Observer pattern brengt de krachten in evenwicht door een extra niveau van indirectie voor te stellen, waarbij klassen zich kunnen registreren voor een bepaald event. Een ander kenmerk dat John Vlissides naar voor brengt is dat patterns helpen om structuur te brengen in een systeem. Omdat patterns een combinatie zijn van probleemsituatie en oplossing, kan het helpen om een bepaald systeem te verdelen in deel-situaties die worden beschreven door patterns. Tegelijk wordt het op die manier makkelijker om een systeem te beschrijven, aan de hand van het vocabularium dat patterns ter beschikking stellen [MYTH]. 2.3.3 Toepassing Patterns zijn niet bedoeld als vervanging van andere software design methodes. Zowel de Gang of Four als anderen zagen de belangrijkste toepassing van patterns als aanvulling op methodes zoals object-georienteerde analyse en design. Waar Coplien vindt dat object-georienteerde analyse soms wordt gereduceerd tot een oefening "vind het object", concentreren patterns zich dikwijls op samenwerkingsverbanden [IPAL]. Patterns kunnen dus bestaande methodes aanvullen met "nuggets of insight". De ongrijpbaarheid van deze "nuggets of insight" is naar mijn bescheiden mening ook een belangrijke handicap van patterns. Patters bestaan er voor analyse, design,
implementatie, architectuur, het software proces, samenwerking tussen mensen, organisatie, enzoverder enzovoort. Soms is er een duidelijk verband, soms lijken ze volledig op zichzelf te staan. Met de explosie van het aantal patterns wordt het steeds moeilijker om op een ongestructureerde manier te grasduinen door de patterns in de hoop enkele te vinden die van toepassing zijn op de huidige situatie. Er zijn inspanningen om patterns op een systematische manier te categoriseren [WIKI][HILLSIDE], en ook de Siemens Gang of Five stelt voor om patterns te verzamelen in wat ze een "pattern system" noemen. Zo'n systeem is equivalent met wat Christopher Alexander een "pattern language" noemt, en moet volgens de Gang of Five aan een aantal criteria voldoen: het moet voldoende patterns bevatten die een voldoende breed gebied bestrijken. Het moet al deze patterns op een uniforme manier beschrijven, en het moet de relaties tussen de patterns aanduiden. Er moet ook een organisatie onder de patterns zijn die het eenvoudig maakt om bepaalde patterns terug te vinden, en het systeem moet aantonen hoe de patterns moeten worden gebruikt. Tenslotte moet het systeem kunnen evolueren door nieuwe patterns of criteria te adopteren, en oude weg te laten vallen [SGOF]. Hoewel patterns duidelijk hun nut al hebben bewezen en dat ook in de toekomst zullen doen, stel ik toch mijn vragen bij de realiseerbaarheid van zo'n "pattern system". Sommige patterns zijn duidelijk verbonden en zoals ook de Gang of Five aantoont, doen sommige patterns niet meer dan een probleem uiteen breken in verschillende andere patterns. Maar ik denk dat patterns bijna per definitie fragmentarisch zijn, en dus moeilijk een volledige taal kunnen vormen, waarbij een hiërarchie van oplossingen op alle niveau's voor alle problemen van een toepassingsgebied wordt aangeboden. Dat neemt niet weg dat patterns een onschatbare bron van ervaring en kennis zijn. Deze kennis kan toegepast worden tezamen met object-georienteerde technieken, en bij extensie ook met component-georienteerde technieken. Om er nog maar eens op terug te komen: het Observer pattern bijvoorbeeld wordt toegepast doorheen de volledige Javabeans API en dus ook binnen het "Magic" project.
3 Eigenschappen en beloften
Alle mooie beloften van software methoden hebben betrekking op de kwaliteit van software, of van het ontwikkelingsproces. Aan de hand van twee definities wil ik het vage woord "kwaliteit" opbreken in verschillende deelaspecten, en vervolgens bespreek ik voor elk van deze aspecten wat component-georienteerd programmeren kan of wil betekenen. Alle definities komen uit het Engels, en in tegenstelling tot de rest van deze thesis zou ik de termen voorlopig graag in het Engels laten staan, omdat er zo al genoeg verwarring over sommige van de woorden is.
De Siemens Gang of Five noemt als niet-functionele kwaliteiten changeability, interoperability, efficiency, reliability, testability, reusability, waarbij ze changeability nog onderverdelen in maintainability, extensibility, restructuring, portability. Reliability wordt onderverdeeld in fault tolerance en robustness [SGOF]. Ghezzi, Jazayeri en Mandrioli definieren er ietsje meer: "correctness, reliability, robustness, performance, user friendliness, verifiability, maintainability, reusability, portability, understandability, interoperability, productivity, timeliness, en visibility" [FUND]. Zij klasseren de kwaliteiten volgens een aantal kenmerken. Intern of extern duidt aan of de kwaliteit vooral voor de programmeur, of van de klant van belang is. Product slaagt op de software zelf, terwijl proces duidt op de manier waarop het product tot stand kwam. Bertrand Meyer vernoemt "correctness, robustness, extendability, reusability, compatibility, efficiency, portability, ease of use, functionality en timeliness" [OOSC2]. Alvorens alle kwaliteiten afzonderlijk te behandelen, nog een opmerking: voor componentgeorienteerde systemen zal elk kwaliteitsaspect een functie zijn van de componenten die het geheel samenstellen. Een rotte appel in de mand zal de kwaliteiten van het hele systeem aantasten, dikwijls zonder dat de ontwerper van het systeem daar veel aan kan doen, bijvoorbeeld omdat de rotte appel in kwestie aangekochte software is. Voor component-systemen die pas op run-time worden samengesteld is er zelfs nog minder controle mogelijk. Dit is een van de grote zwakke punten van component-georienteerd programmeren en het wordt door iedereen onderkend [SDM].
3.1 Correctness (correctheid) Correctheid werd niet door de Siemens Gang of Five genoemd, om de simpele reden dat het geen niet-functionele kwaliteit is. Voor Ghezzi en co. is correctheid een absolute kwaliteit: ofwel beantwoordt het programma aan de functionele specificaties, ofwel niet. Er is geen tussenweg. Correctheid is een externe kwaliteit van het product. In een systeem gebouwd op componenten moet elk component correct zijn opdat het volledig systeem zelfs nog maar een kans kan maken om in zijn totaliteit correct te zijn. Als het systeem op run-time componenten toelaat, kan statisch nooit bepaald worden of het systeem correct is. Wat wel statisch bepaald kan worden, is of het systeem correct is, gebaseerd op de interfaces die gelden tussen de verschillende componenten. Met andere woorden: zou het systeem correct zijn als alle componenten hun interface correct zouden implementeren? Zowel Szyperski als Meyer zijn het erover eens dat interfaces, of contracten op dit moment nog niet gedetailleerd genoeg zijn om dit te kunnen bepalen. Omdat correctheid voor een groot deel een kwestie van semantiek is, valt deze eigenschap moeilijk te vatten in interfaces. Toch is het vooral hier dat Szyperski en Meyer nog vooruitgang verwachten [SDM].
3.2 Reliability (betrouwbaarheid) Betrouwbaarheid is voor de Gang of Five het vermogen van een systeem om te functioneren, zelfs als fouten optreden. Ze delen gebeurlijke fouten op in voorziene fouten ("fault tolerance"), en onvoorziene fouten ("robustness"). Voor Ghezzi en co. is betrouwbaarheid echter iets heel anders: het is een zwakkere vorm van correctheid. Een systeem kan strikt gezien incorrect zijn, maar toch als betrouwbaar worden beschouwd omdat de incorrectheid niet zo belangrijk is, of slechts zelden voorkomt. Wat de Gang of Five dus "fault tolerance" noemt, valt bij Ghezzi en co. onder correctheid, en gedeeltelijk onder betrouwbaarheid, omdat gedrag bij voorziene fouten onderdeel vormt van de gevraagde functionaliteit. Betrouwbaarheid als zwakkere vorm van correctheid is ook weer een functie van alle componenten, en daar zijn dezelfde gevaren aan verbonden. Toch kan betrouwbaarheid verhoogd worden via het ontwerp, bijvoorbeeld door redundancy in te bouwen, monitors, en regelmatige controle. Component-georienteerd programmeren vereist dus een extra inspanning (in interfaces, documentatie en code) om een kwaliteitsaspect als betrouwbaarheid zelfs nog maar op vergelijkbaar niveau te krijgen van een monolithisch programma. Hoewel dit vermoeden niet steunt op literatuur of enige andere aanwijzing, kan ik me toch voorstellen dat de betrouwbaarheid zal stijgen tezamen met de overstap naar componentgeorienteerde software. Component-georienteerd programmeren vergt sowieso een extra inspanning. Een bedrijf dat bereid is deze extra inspanning te leveren, zal hoogstwaarschijnlijk ook willen investeren in betrouwbaarheid. 3.2.1 Fault tolerance (fout tolerantie) Dit kwaliteitsaspect wordt alleen gedefineerd door de Gang of Five. De andere twee brengen deze eigenschap onder correctheid (als de fouten voorzien zijn), of onder robustness (als de fouten niet voorzien waren of de reactie niet gespecifieerd). Een belangrijke bron van fouten bij een gedistribueerd systeem zoals CORBA of COM ligt bij het netwerk. Het systeem kan in delen uiteenvallen, of de "broker" kan onbereikbaar zijn. Zelfs als het netwerk operationeel is, kan het zijn dat momenteel een bepaald cruciaal component niet gevonden wordt door de "broker". Dit zijn allemaal fouten die makkelijk te voorzien zijn, en dus op een correcte manier moeten worden afgehandeld door ieder zichzelf respecterend component-georienteerd systeem. Ook hier geldt dus dat een extra inspanning is vereist om een fout tolerantie te bereiken die vergelijkbaar is met niet-gedistribueerde systemen.
Langs de andere kant is de extra foutgevoeligheid gebonden aan het gedistribueerd zijn, en niet zozeer aan componenten. De explosieve populariteit van het internet en internettoepassingen leren dat de voordelen van gedistribueerde applicaties opwegen tegen het nadeel van verhoogde foutgevoeligheid, om nog te zwijgen van veiligheidsaspecten.
3.2.2 Robustness (robuustheid) Robuustheid is een ongrijpbare geest voor alle geraadpleegde auteurs, maar in hun vaagheid komen de definities dus wel grotendeels overeen. Het beschrijft het gedrag van het systeem onder onvoorspelbare situaties. Als een samenstelling van componenten volledig correct kan beschouwd worden op basis van de contracten, dan valt het gros van de resterende fouten hoofzakelijk binnen twee categorieën. Er zijn fouten inherent aan het gedistribueerde karakter van componenten, en er zijn de fouten veroorzaakt door specifieke componenten omdat ze niet beantwoorden aan hun contract. Beide vallen onder de categorie correctheid (of eventueel fout tolerantie). Als een fout ontstaat doordat twee componenten een verschillende semantische interpretatie geven aan hun contract, is de aard van de fout in wezen onvoorspelbaar, en valt het onder robuustheid. Gegeven de huidige interface talen is dit soort fouten inderdaad waarschijnlijk. Daarmee schuift de fout wel meer in de richting van categorie fout tolerantie. Als we alle voorheen genoemde fouten buiten beschouwing laten, blijven alleen de volledig onvoorspelbare fouten over. De onvoorspelbaarheid laat niet toe om veel veronderstellingen te maken, maar persoonlijk kan ik me voorstellen dat een componentgeorienteerd systeem er lichtjes robuuster op wordt, doordat de aanpak van verschillende componenten kan verschillen. Er zal waarschijnlijk een zekere redundantie bestaan binnen het systeem die het ook waarschijnlijker maakt dat een fout wordt ontdekt en/of afgehandeld wordt. Bovendien, vanwege de losse "coupling" tussen de componenten, is er kans dat een fout gelokaliseerd blijft, of dat het systeem verder kan gaan met beperkte functionaliteit.
3.3 Interoperability/compatibility (samenstelbaarheid) Zowel de Gang of Five als Ghezzi en co. noemen het "interoperability", Meyer noemt het "compatibility", en ik noem het samenstelbaarheid. Programma's zijn samenstelbaar als het mogelijk is om data van een programma te gebruiken in een ander programma.
Bij component-georienteerde systemen bestaat de kwaliteit "samenstelbaarheid" op twee niveau's: het systeem zelf kan samenstelbaar zijn met andere systemen, en individuele componenten kunnen samenstelbaar zijn met andere componenten. Voor individuele componenten maakt samenstelbaarheid deel uit van de definitie. Ik zou zelfs zover willen gaan om te zeggen dat het succes van de hele component-scene afhangt van de samenstelbaarheid van aparte componenten. Hoe minder "wiring" noodzakelijk is om niet-triviale verbindingen te maken tussen twee componenten, hoe beter het component-georienteerd paradigma tot zijn recht komt. Alle kwaliteiten van componenten staan in zekere zin ten dienste van de samenstelbaarheid, terwijl de samenstelbaarheid van een component in grote mate bepaalt in hoeverre het mogelijk is om kwalitatieve totaalsystemen te bouwen. Op het niveau van het hele systeem is samenstelbaarheid geen inherente kwaliteit meer, maar wel gewenst. Maar omdat het systeem is opgebouwd uit componenten die in principe samenstelbaar en vervangbaar zijn, verwacht ik persoonlijk dat samenstelbaarheid op zijn minst makkelijker realiseerbaar zal zijn, zelfs op niveau van het hele systeem.
3.4 Efficiency/performance (efficientie) Efficientie wordt door alle auteurs gedefineerd als de verhouding van aangeboden functionaliteit tegenover gebruikte resources zoals processortijd, geheugen, en andere. Efficientie is typisch gezien niet het sterke punt van componenten. Als ik mijn eigen definitie als leidraad mag nemen, bevatten componenten "extra inspanningen" in de vorm van code. Er zal dus meer code in een component zitten dan strikt noodzakelijk voor de gevraagde functionaliteit. Deze extra code kan zich vertalen in langere uitvoertijd, grotere geheugenvereisten, enzoverder. Niet alleen op schaal van de componenten, maar ook in het totale systeem zal een zekere redundantie niet te vermijden zijn. Als het ene component zijn postconditie controleert en het andere zijn preconditie, dan is dezelfde gegevensstroom hoogstwaarschijnlijk twee keer gecontroleerd. Controle die voor een monolithisch systeem op compile-time kan gebeuren moet bij een component-georienteerd systeem op run-time gebeuren. Ook het systeem op zich zal dus hoogstwaarschijnlijk meer uitvoertijd en geheugen vragen. Om de samenstelbaarheid van een systeem te verhogen zal een component-georienteerd systeem dikwijls ook extra abstractie-lagen en niveau's van indirectie bevatten. Elk van deze lagen neemt zowel processortijd als geheugen in beslag. Tenslotte heeft een componenten systeem veel weg van een gedistribueerd systeem. Als componenten dan ook effectief via een netwerk (of zelfs nog maar via IPC) worden
aangesproken, is de tijd die nodig is om data heen en weer te sturen vele ordes groter dan een simpele procedure-aanroep. Ook dit neemt extra tijd in beslag.
3.5 Testability/verifiability (verifieerbaarheid) "Testability" wordt door de Gang of Five gedefinieerd als de mate waarin het mogelijk is om na te gaan of een systeem correct is. Ghezzi en co. omvatten deze eigenschap met het woord "verifiability", maar zij hechten er een iets wijdere betekenis aan: "verifiability" is de mate waarin kan worden nagegaan in hoeverre het systeem elk van de hier opgesomde kwaliteitsaspecten bevat. Meyer noemt geen enkel begrip dat vergelijkbaar is. Ook voor verifieerbaarheid geldt vooral dat een systeem maar kan nagegaan worden voor zover het al samengesteld is. Als een componenten-systeem toelaat dat nieuwe componenten op run-time worden toegevoegd, kunnen er onmogelijk op voorhand definitieve voorspellingen worden gedaan omtrent kwaliteitsaspecten. Verifieerbaarheid van een component-georienteerd systeem is in het algemeen dus lager dan van een monolithisch systeem. Verifieerbaarheid op systeem-niveau beperkt zich tot de specificaties van de interfaces. Een krachtige IDL is dus belangrijk om zoveel mogelijk eigenschappen van het systeem al op voorhand (statisch) te kunnen verifieren. Szyperski en Meyer zijn beiden van mening dat krachtiger IDL's wenselijk zijn om de verifieerbaarheid te verhogen [SDM]. Verifieerbaarheid op het niveau van een enkel component kan soms ook beperkt zijn, omdat een component tenslotte bedoeld is om samen te werken met andere componenten. Een server testen bijvoorbeeld vereist dat er een client is om de server aan te spreken. Bij incorrect gedrag is het niet noodzakelijk duidelijk of de fout bij de server, de client, of bij verbindende componenten ligt [SGOF].
3.6 Reusability (herbruikbaarheid) De Siemens Gang of Five citeert Goldberg's definitie van "reuse": "the act of achieving what is desired with the help of what already exists", maar in tegenstelling tot deze definitie beperken ze de toepassing van hergebruik tot de software. Zij onderscheiden daarbij twee aspecten bij het ontwikkelen van software: ontwikkeling met behulp van hergebruik, en ontwikkeling met hergebruik als doel [SGOF]. Zowel Ghezzi en co. als Meyer beschouwen ook andere vormen van hergebruik: hergebruik van kennis bijvoorbeeld, doordat dezelfde mensen bij een ander project worden ingezet. Hergebruik van analyse, doordat een probleem als gelijkaardig wordt herkend aan een vroeger geanalyseerd probleem.
De meeste vormen van hergebruik zijn een keuze van het bedrijf, en dus eerder een interne kwaliteit van het proces. Specifiek hergebruik van software is een van de belangrijkste doelstellingen van component-georienteerd programmeren. Dat maakt het eerder tot een externe dan een interne kwaliteit. Hergebruik op systeem-niveau wordt bevorderd door de vele lagen van abstractie, en door de vervangbaarheid van de afzonderlijke componenten. Het is mogelijk om de functionaliteit van een bestaand systeem aan te passen door componenten te vervangen door componenten die de bestaande interfaces respecteren, maar daarnaast nog extra functionaliteit bieden. Hier speelt ook overerving en uitbreiding van interfaces een rol. Hergebruik van afzonderlijke componenten wordt bevorderd door de scheiding van interface en implementatie, en door sterke contracten die zeer duidelijk specifieren wat een bepaald component doet. Hergebruik van een specifiek component is afhankelijk van twee dingen: de kracht van de IDL, en het vakmanschap van de ontwerper van een component bij het vastleggen van de functionaliteit. Een component mag tenslotte volgens alle gangbare definities "goed" geprogrammeerd zijn, als de enige functie van het component is om de diameter te berekenen van het zwarte gat dat zich het verst van de Aarde bevindt, zal het component hoogstwaarschijnlijk niet veel opnieuw worden gebruikt. Hergebruik binnenin componenten wordt bevorderd door object-georienteerde technieken zoals overerving en polymorphisme. Het component-georienteerd paradigma formuleert geen nieuwe wondermiddeltjes om stukken van componenten opnieuw te kunnen gebruiken. Als een deel van de code van een component opnieuw wordt gebruikt door middel van overerving, valt dit volgens Szyperski onder de noemer "implementation inheritance". Hier zijn enkele gevaren aan verbonden, en bovendien vereist het een inzicht in de implementatie van het component, wat indruist tegen het principe van inkapsulatie. Szyperski stelt dan ook voor om functionaliteit te delegeren [SZYP]. Los daarvan is herbruikbaarheid geen automatisch gevolg van gelijk welke methodologie. Hergebruik hangt sterk af van de individuele programmeurs, van het management, van de huidige software methodes, van de grootte en het budget van het bedrijf, van allerlei menselijke factors, en niet in het minst van de "process maturity". Hergebruik is een doel waaraan gewerkt moet worden, en geen bonus die in de schoot komt vallen [DIV][IMP].
3.7 Changeability (veranderbaarheid) Veranderbaarheid wordt door de Siemens Gang of Five gedefinieerd als de eenvoud waarmee veranderingen aan het programma kunnen worden uitgevoerd nadat de eerste ontwikkelingsfase. Ze verdelen veranderbaarheid op in vier subcategorieen. Ghezzi en co. gebruiken de term "maintainability" als algemene noemer, en splitsen dit op in "corrective maintainance", "adaptive maintainance" en "perfective maintainance". Meyer spreekt alleen maar van "portability" en "extendability", en kiest dus om enkele aspecten te negeren.
Persoonlijk vind ik de indeling van de Gang of Five het meest geslaagd, omdat ze de andere twee omsluit, en uitbreidt. Ghezzi en co. wijzen er wel op dat de classificatie van veranderingen niet altijd even rechttoe-rechtaan is omdat de oorspronkelijke specificaties bijvoorbeeld niet altijd even duidelijk zijn. Een aanpassing om die reden zweeft dan ergens tussen correctie en uitbreiding [FUND]. Specificaties kunnen ook nog eens veranderen naarmate het project vordert en het dus duidelijk wordt welke de mogelijkheden zijn. 3.7.1 Maintainability/repairability (corrigeerbaarheid) Corrigeerbaarheid duidt op het gemak waarmee bestaande fouten kunnen worden verbeterd in een systeem. [SGOF] gebruikt de algemene term "maintainability" om een specifiek aspect van post-sales services mee aan te duiden. Volgens Ghezzi en co. laat de term "maintainance" ruimte voor misverstanden, omdat het suggereert dat software verslijt, net zoals mechanische onderdelen. Daarom kiezen zij voor de term "repairability". Meyer laat na om iets gelijkaardig als kwaliteitsaspect te definieren, en dit is in mijn ogen een vergissing. Hij duidt zelf aan dat het corrigeren van fouten ongeveer 20% van de diensten na verkoop uitmaken [OOSC2], en ik vond in zijn tekst geen goede reden om corrigeerbaarheid te negeren als kwaliteitsaspect. Zijn stelling is wel dat via modularisatie en abstracte datatechnieken een programma zo kan worden geschreven dat (ook correctieve) aanpassingen sterk lokaal blijven. Volgens mij definieert Meyer onderhoudbaarheid op die manier dus WEL als een kwaliteitsaspect. Corrigeerbaarheid van individuele componenten wordt sterk bevorderd doordat het component-georienteerde paradigma sterk leunt op principes zoals modulariteit. De scheiding tussen interface en implementatie en bijgevolg de vervangbaarheid van componenten vergemakkelijkt de vervanging van een component door de nieuwe, gecorrigeerde versie. Aangekochte componenten zijn in principe minder corrigeerbaard dan eigen software. Indien het defecte component aangekochte software is moet natuurlijk gewacht worden op een correctie van het bedrijf dat het component oorspronkelijk maakte. In die zin hangt de corrigeerbaarheid van een component rechtstreeks af van de bereidwilligheid van het andere bedrijf om correcties aan te brengen. In het slechtste geval is het nog altijd mogelijk om de aangekochte software weg te gooien en te vervangen door een intern ontwikkeld component. Corrigeerbaarheid op niveau van het hele software systeem ligt een beetje moeilijker. Als het defect is terug te brengen tot enkele componenten, komen de voordelen van modulariteit en vervangbaarheid weer naar boven, en hangt alles af van de corrigeerbaarheid van afzonderlijke componenten. Als het defect daarentegen dieper ligt, bijvoorbeeld omdat bestaande interfaces onvoldoende of verkeerd blijken, wordt corrigeerbaarheid een kwestie van herstructurering, besproken onder 3.7.4.
3.7.2 Extensibility/extendability/perfective maintainability (uitbreidbaarheid) Ze gebruiken lichtjes verschillende termen, maar iedereen is het erover eens dat een systeem moet kunnen uitgebreid worden, en dat het daarop voorzien moet worden. Zoals Parnas het noemt: "design for change". Uitbreiding betekent het toevoegen van functionaliteit die niet in de oorspronkelijke vereisten was vernoemd. Samen met herbruikbaarheid is uitbreidbaarheid een belangrijke doelstelling van de huidige ontwerpmethoden. Uitbreidbaarheid werd niet in de definitie van componenten genoemd, maar het is een gevolg van de samenstelbaarheid van componenten. Door samenstelling van meerdere componenten wordt de totale functionaliteit groter dan de som van het geheel. Een andere vorm van uitbreidbaarheid wordt mogelijk gemaakt via object-georienteerde technieken. door het overerven van een interface (of een volledig component) kan een nieuw component worden bijgevoegd aan een bestaand systeem. Dit nieuw component kan naast de oorspronkelijk geplande functionaliteit ook nog extra functionaliteit aanbieden. Uitbreidbaarheid van een volledig software systeem is in zekere zin net zoals herbruikbaarheid: vereist zorgvuldige planning, zowel in het oorspronkelijk ontwerp als bij de opeenvolgende uitbreidingen [FUND]. Factors die genoemd werden bij uitbreidbaarheid (zoals programmeurs, steun van het management, steun van het bedrijf, de "process maturity", enz.) spelen hier dus een vergelijkbaar belangrijke rol. 3.7.3 Portability/adaptive maintainability (portabiliteit) Portabiliteit (de minst gelukkige vertaling uit het rijtje) is het vermogen van een software systeem om de functioneren in een verschillende omgeving met een minimum aan inspanning. Meyer, en ook Ghezzi en co. beperken hun definitie van "omgeving" tot de hardware en (versies van) het "operating system". De Gang of Five rekenen ook "user interfaces", compilers, en programmeertalen tot de omgeving. Dit is een interessante toevoeging, en zeker niet triviaal. De Javabeans standaard voorziet bijvoorbeeld expliciet dat een systeem al of niet een grafische "user interface" bevat. Hoewel Sun beweert met Java een platform-onafhankelijke omgeving geschapen te hebben, is de "virtual machine" uiteindelijk ook maar een platform op zich, en bovendien verschillen de opeenvolgende versies soms grondig in hun mogelijkheden. Al deze verschillen overbruggen maakt deel uit van de portabiliteit van een product. Om makkelijk overdraagbaar te zijn, moeten alle elementen die afhankelijk zijn van de omgeving zoveel mogelijk geconcentreerd worden in speciale componenten [SGOF].
Zulke design-technieken maken niet noodzakelijk deel uit van het componentgeorienteerd paradigma. "Request brokers" pakken het probleem van portabiliteit meer zijdelings aan. Interfaces worden gespecifieerd in een "Interface Definition Language", waarvoor bindingen zijn voorzien naar de meeste populaire talen. Op die manier kunnen een client en een server met elkaar communiceren via een gemeenschappelijke interface, zelfs al draaien elk op een totaal verschillende omgeving. Het is dus niet meer absoluut noodzakelijk dat alle componenten in een bepaalde omgeving draaien, opdat hun diensten bereikbaar zouden zijn vanuit die omgevingen. Zolang de juiste bindingen bestaan naar een programmeertaal, kunnen vanuit die taal alle componenten worden aangesproken die bereikbaar zijn voor een "object request broker".
3.7.4 Restructuring (herstructureerbaarheid) "Restructuring" wordt alleen door de Siemens Gang of Five gedefinieerd. Zij omschrijven het als de reorganisatie van een software systeem en de relaties tussen de verschillende delen van het systeem. Persoonlijk lijkt het mij nuttig om deze activiteit apart te vernoemen, omdat ze vrij grondig verschilt van andere activiteiten. Een herstructurering komt natuurlijk alleen: meestal dient ze ter ondersteuning van andere veranderingen, bijvoorbeeld optimalisatie of om verdere uitbreiding mogelijk te maken. Maar de structuur veranderen vereist andere eigenschappen van een systeem dan bijvoorbeeld foutencorrectie of uitbreiding. Een component-georienteerd systeem heeft typisch gezien een losse coupling tussen de verschillende componenten. Dit kan een herstructurering makkelijker maken. In principe hangt de moeilijkheidsgraad van een herstructurering hier af van de "wiring", omdat die bijna altijd zal moeten aangepast worden. Als de "wiring" uit code bestaat die grotendeels automatisch is gegenereerd, zou herstructurering grotendeels door speciale tools kunnen uitgevoerd worden. Een zeer aantrekkelijk beeld, al twijfel ik persoonlijk aan de haalbaarheid ervan. Als de herstructurering echter ook een verandering in de interfaces noodzakelijk maakt, kunnen er wel eens meer moeilijkheden optreden. Sommige interfaces kunnen bijvoorbeeld gepubliceerd zijn en het zou bepaalde componenten kunnen "breken" om de interface te veranderen. Het is ook niet triviaal om de interface te veranderen voor een component dat aangekocht is, en waarvan de implementatie dus niet kan aangepast worden. Het herstructureren van een software systeem is altijd een ingrijpende gebeurtenis die zelden kan doorgevoerd worden zonder dat er implementaties van componenten moeten worden gewijzigd. Langs de ene kant kan een losse coupling van componentgeorienteerde software de herstructurering makkelijker maken, langs de andere kant is
het moeilijk of onmogelijk om aangekochte software te veranderen, of gepubliceerde interfaces te wijzigen.
3.8 User friendliness/ease of use (gebruiksvriendelijkheid) Meyer definieert gebruiksgemak als het gemak waarmee gebruikers de aangeboden functionaliteit kunnen aanleren en aanwenden. Hij duidt op de subjectiviteit van de kwaliteit, en vermeldt ook hoe moeilijk (of onmogelijk) het is om uit te maken door wie de software zal worden gebruikt. Ghezzi en co. nemen de definitie van gebruiksvriendelijkheid iets verder dan de "user interface", en noemen een stuk software dat zich makkelijk laat inzetten ook gebruiksvriendelijk. Zij stellen ook dat gebruiksvriendelijkheid een functie is van andere kwaliteiten zoals correctheid en robuustheid. Een fancy interface die de verkeerde antwoorden geeft kan inderdaad moeilijk gebruiksvriendelijk genoemd worden. Gebruiksvriendelijkheid wordt door de Siemens bende waarschijnlijk als een functionele kwaliteit beschouwd, want ze vermelden het niet in hun lijstje. Gebruiksvriendelijkheid voor componenten kent twee kanten, omdat er twee soorten gebruikers zijn. De eindgebruiker is degene die het software systeem uiteindelijk zal gebruiken, en wiens wensen hoogstwaarschijnlijk zijn bestudeerd in de analyse fase. Voor de eindgebruiker is de bediening van belang, de grafische interface, en een consistente aanpak doorheen het software systeem, en daarmee negeren we even de "features" zoals "undo". Een consistente aanpak kan echter moeilijk te realiseren zijn in een componentgeorienteerd systeem. Als we componenten tijdelijk zien (!) als elementen met een eigen grafische voorstelling, dan kan het moeilijk zijn om op een consistente manier visuele hints te geven (bv. bepaalde kleuren voor bepaalde toestanden). De andere gebruiker is de programmeur die een component gebruikt in zijn eigen systeem. Gebruiksvriendelijkheid is daar een functie van documentatie, robustheid, helderheid van eventuele foutmeldingen, parameter controle, en dergelijke. Gebruiksvriendelijkheid is hier ook een functie van de andere kwaliteiten van het product. Als interne kwaliteit van het product, is het bovendien sterk gerelateerd aan wat Ghezzi en co. "understandability" noemen.
3.9 Understandability (eenvoud) Eenvoud wordt alleen vernoemd wordt door Ghezzi en co., en ze staat voor hoe makkelijk het is om de software te begrijpen. Eenvoud is duidelijk sterk gerelateerd aan gebruiksgemak, en beide zijn moeilijker te verwezenlijken naarmate de software een inherent complex probleem behandelt. Toch is het waard ze apart op te sommen, omdat
eenvoud aan de basis ligt van vele andere kwaliteiten, zoals uitbreidbaarheid en verifieerbaarheid [FUND]. Eenvoud van een component hangt af van de functie die het uitvoert, maar ook van de inspanningen op gebied van interface, documentatie en code. Voor het gebruik van een component is het niet noodzakelijk om de implementatie te verstaan. Dit zou het verstaan van een component op zijn minst gedeeltelijk moeten vereenvoudigen. Voor de eenvoud van een volledig systeem is de component-georienteerde manier van programmeren een tweesnijdend zwaard. Component-georienteerd programmeren is een modulaire manier van programmeren. Modulariteit is een manier om complexe systemen in abstracte stukken te verdelen zodat de stukken beter verstaanbaar zijn. Het heeft ook tot gevolg dat overzicht moeilijker te bewaren valt. Dit nadeel is alleen aanvaardbaar voor systemen van voldoende complexiteit. Dit was al zo voor procedureel programmeren en object-georienteerd programmeren, dus een grote verassing is het niet. Zaak is om een soort grens te vinden waarbij component-georienteerd programmeren de zaak kan vereenvoudigen.
3.10 Productivity (productiviteit) Ook productiviteit is een kwaliteit die alleen vernoemd wordt in [FUND], en het slaat eerder op het software ontwikkelingsproces. Het is het belangrijkste streven van software bedrijven, maar ook een van de moeilijkst te meten kwaliteiten. Component-georienteerd programmeren belooft een verhoogde productiviteit door middel van hergebruik, betere corrigeerbaarheid, grotere veranderbaarheid en meer uitbreidbaarheid. Langs de ene kant vergt elk component afzonderlijk veel meer inspanning (zie definitie) dan simpelweg een stuk code dat bepaalde functionaliteit moet vervullen. Een volledig component-systeem van scratch opbouwen zal dus resulteren in lagere productiviteit. De winst in productiviteit kan gerealiseerd worden als een deel van de ontwikkelingskosten kunnen worden vermeden door hergebruik, of als de ontwikkelingskosten van afzonderlijke componenten kunnen gerecupereerd worden via verkoop van deze componenten -- om opnieuw gebruikt te worden. Zonder hergebruik zal de productiviteit juist sterk dalen.
3.11 Timeliness (tijdigheid) Tijdigheid van de levering van het product is een externe kwaliteit van het product, zoals gedefinieerd door [FUND]. De twee andere geraadpleegde boeken noemen deze kwaliteit niet. Het houdt rechtstreeks verband met de betrouwbaarheid van het ontwikkelingsproces, en het is een van de redenen geweest om software ontwikkeling meer op "software engineering" te laten lijken [FUND]. Via een verhoogde productiviteit dankzij de mogelijkheid om tijd te winnen door componenten te kopen in plaats van intern te ontwikkelen belooft component-georienteerd programmeren een verbetering van deze kwaliteit.
3.12 Visibility (transparantie) Transparantie beduidt hoe makkelijk het is om de impact van bepaalde beleidsbeslissingen op het productie proces in te schatten. Het staat in rechstreeks verband met tijdigheid [FUND]. Ik zie geen reden waarom component-georienteerd programmeren automatisch een transparanter productie-proces zou opleveren dan zijn neef, object-georienteerd programmeren. De meeste technieken voor analyse, design en implementatie zijn sterk gelijkaardig, dus ook het productie-proces zal gelijkaardig zijn.
4 Huidige situatie
Er zijn in heden en verleden al enkele software systemen ontwikkeld die alle kenmerken van component-georienteerd programmeren in zich dragen. In de volgende hoofdstukken wil ik kort over enkele van deze overgaan, en daaruit concluderen dat componentgeorienteerd programmeren commercieel mogelijk is, al zal het paradigma waarschijnlijk niet doordringen tot in de kleinste uithoekjes van alle mogelijke software applicaties.
4.1 Besturingssystemen Besturingssystemen kunnen worden gezien als de oudste component-georienteerde systemen [SZYP]. Het zijn software systemen die bedoeld zijn om op run-time te worden uitgebreid met binaire modules die geleverd kunnen zijn door een derde partij, en die aan
een aantal voorwaarden moeten voldoen, zoals een maximum vereist geheugen. Alle programma's kunnen interageren met het systeem via op voorhand gedefinieerde routines, "system calls", die uitgebreid zijn gedefinieerd in handboeken voor programmeurs. Meestal is communicatie met andere programma's mogelijk, op zijn minst via files. Besturingssystemen zijn ook een goed voorbeeld van "component frameworks" die zeer weinig onderlinge compatibiliteit vertonen, en toch commercieel rendabel zijn. Er zijn zeer veel verschillende besturingssystemen, elkaars programma's niet kunnen draaien, op enkele speciaal voorziene compatibiliteiten na. Een win32 programma draait niet onder een x-windows omgeving, een Linux console programma kan niet gestart worden van de DOS prompt. Sterker nog: het ene UNIX-achtige besturingssysteem kan niet noodzakelijk programma's draaien van een andere UNIX-"flavor", al zijn ook daar speciale inspanningen geleverd. Een andere bron van incompatibiliteit is de onderliggende hardware. Een programma geschreven voor Windows NT op een Intel processor kan niet draaien onder een Windows NT voor een ALPHA processor. Vanuit het standpunt van component-georienteerd programmeren is een typisch besturingssysteem helemaal niet zo'n veelzijdig "component framework". Programma's kunnen niet helemaal willekeurig worden gecombineerd, "versioning" is een probleem dat elk programma voor zichzelf moet oplossen, installatie en desinstallatie is ook ieder voor zich, de werking van de programma's interfereert soms met elkaar in ongewenste vormen, en communicatie tussen programma's is zoniet moeilijk, dan toch kostelijk en ook weer weinig gestandardiseerd van systeem tot systeem, al komt daar verandering in. Ondanks de incompatibiliteit en andere genoemde problemen is er toch duidelijk een bloeiende markt voor programma's. Dit duidt erop dat een eventuele "wiring" standaard helemaal alles-omvattend moet zijn om toch levenskrachtig te zijn. Zowel Szyperski als Meyer duiden -in een analogie- zelfs op de gevaren van een alles-omvattende "wiring" standaard [SDM][SZYP].
4.2 Plug-in architecturen Vele huidige programma's kunnen uitgebreid worden met zogenaamde plug-ins, stukken code die kunnen bekomen worden via het internet, van andere software ontwikkelaars die de functionaliteit van het gast programma uitbreiden. Netscape, Internet Explorer, ICQ, Winamp, Photoshop, 3D Studio MAX, the Gimp, het zijn slechts een handvol uit de volledige lijst. Voor Netscape en Internet Explorer bestaan zelfs al verschillende plug-ins die zelf ook weer kunnen worden uitgebreid worden met plug-ins. Dit is het begin van wat Meyer ziet als een recursief patroon van componenten binnen componenten [SDM]. Deze plug-ins zijn meestal maar voor een enkel programma bedoeld, en kunnen niet worden toegepast op de andere. Zelfs plug-ins voor twee functioneel gelijkaardige programma's als Netscape en Internet Explorer hebben meestal lichtjes aangepaste
eigenschappen. Een "skin" voor Winamp kan niet worden toegepast op ICQ. Een fotofilter voor Photoshop kan niet worden gebruikt binnen Netscape. Communicatie tussen verschillende plug-ins is erg beperkt. Bij Winamp is er natuurlijk een stroom van informatie van de plug-in die het geluid genereert naar de plug-in die de beelden genereert. Bij plug-ins in Photoshop is het mogelijk om verschillende plug-ins los te laten op dezelfde bitmap, maar verder gaat de communicatie niet. Bij browsers is de communicatie tussen plug-ins bijna onbestaande (behalve een koppeling tussen JavaScript en Java applets), en communicatie met de rest van het internet wordt meestal verboden om privacy-redenen. Ook hier geldt dat ondanks de beperkingen toch een bloeiende markt is gegroeid voor plug-ins. Alles hangt natuurlijk af van de doelgroep, en van de functie van het programma. Plug-ins voor recreatief bedoelde programma's zoals ICQ en Winamp zullen geen grote omzet realiseren, en worden meestal gratis verspreid. Maar plug-ins voor grafische toepassingen zoals Photoshop en 3D Studio leveren grof geld op.
4.3 Visual Basic VB is een van de eerste talen geweest om een soort component-structuur te definieren. De eenheid van hergebruik was een VBX control. Zo'n control, dikwijls met een grafische representatie, kan op een "form" worden geplaatst. Bepaalde attributen kunnen worden ingesteld en voor vastgelegde "events" kunnen bijbehorende stukken code worden geschreven. Deze code geldt ook als de "wiring" tussen de verschillende VBX'en. De eerste VBX controls waren niet meer dan grafische widgets zoals knoppen, lijsten en bitmaps. Het onderliggende model was ook alleen voorzien op dit soort eenvoudige gadgets, en laat bijvoorbeeld niet toe dat een component op recursieve wijze andere componenten bevat. Communicatie tussen de verschillende componenten gebeurt via de programmeertaal. Merk ook op dat de eerste versies van Visual Basic nog sterk aanleunden bij hun procedurele aanhangers, GW-BASIC en QuickBasic. In VB 3.0 (een versie waar ik zelf mee gestoeid heb, enkele jaren geleden) bestonden wel al modules, en de componenten werden aangesproken via een notatie die sterk deed denken aan object-orientatie (
.), maar object-georienteerde principes zoals polymorphisme, overerving en inkapsulatie waren op geen enkele manier geïmplementeerd in Visual Basic. VB heeft een eenheid van hergebruik gedefinieerd die, hoewel beperkt, een onverwacht grote markt heeft doen ontstaan. Andere bedrijven begonnen al gauw veel gesofistikeerder controls te programmeren en verkopen, zoals volledige spreadsheets, tekstverwerkers, database-modules enzoverder. Een beperkt (of technisch inferieur) model werkt dus niet noodzakelijk remmend [SZYP]. Ik zou zelfs zo ver durven gaan om
te zeggen dat het simplistisch concept een belangrijke factor was in het succes van Visual Basic. In de latere versies van de programmeertaal is de definitie van een control dan uitgebreid naar respectievelijk OLE controls en later ook COM en ActiveX controls, die veel meer mogelijkheden bieden, en dus nog meer component-georienteerd zijn.
4.4 CORBA/OMA De Object Management Group (OMG) is een samenwerking tussen toonaangevende bedrijven die als doel heeft om object-georienteerde samenwerking mogelijk te maken tussen verschillende talen, besturingssystemen en hardware platforms. Het belangrijkste en bekendste resultaat is de "Common Object Request Broker Architecture". De CORBA maakt het mogelijk de methoden aan te roepen van een object dat in een andere programmeertaal, voor een andere besturingssysteem of voor andere hardware kan gecompileerd zijn, of zich simpelweg op een andere computer bevindt. In essentie is een CORBA dus een systeem voor "remote method invocation" (RMI). RMI brengt verscheidene problemen met zich mee. Omdat elk object zich in een verschillend systeem kan bevinden, en dus met verschillende threads, moeten de objecten voorzien zijn op concurrente toegang. Daarnaast moeten referenties die verwijzen naar een bepaald object op een bepaalde machine (meestal een pointer) vertaald worden naar een uniek adres dat ook mening heeft buiten de machine in kwestie. Om dergelijke problemen aan te pakken heeft de OMG verschillende diensten gedefinieerd bij CORBA. Diensten die nauw verbonden zijn met CORBA als RMI omvatten een "naming service" om objecten te kunnen refereren in contexten groter dan een enkele machine. Er is een "security service" noodzakelijk om op een veilige manier externe componenten te kunnen gebruiken, er is een "trader service" die objecten kan vinden gebaseerd op de diensten die ze aanbieden, een "transaction service" om "rollbacks" mogelijk te maken in een gedistribueerde omgeving, "concurrency service" die synchronizatie over verschillende machines mogelijk maakt, een "change management service" die "versioning" mogelijk zou maken maar waarover ik geen documentatie vond, "externalization services", een "licensing service", en nog verschillende andere (16 in totaal). 4.4.1 Overerving en hergebruik Het OMG object model laat meervoudige overerving toe. "Implementation inheritance" wordt gebruikt om CORBA objecten te voorzien van bepaalde functionaliteit, maar dit gebeurt niet noodzakelijk op een voor de programmeur zichtbare wijze [LEWAN].
Elk object kan slechts een interface implementeren, maar deze interface kan een samenstelling zijn van verschillende interfaces, zodat een object meerdere diensten kan aanbieden. De drie soorten overerving gedefinieerd door [SZYP] zijn mogelijk. Verder zijn ook polymorphisme en inkapsulatie gedefinieerde bewerkingen. 4.4.2 Versioning Een van de "CORBA services" is de "change management service", die ervoor bedoeld is om veranderingen van contract te organiseren, en om verschillende versies tesamen te kunnen laten bestaan. In 1997 was deze standaard nog niet gedefinieerd [SZYP], en ik heb geen informatie hierover kunnen vinden op de OMG site [OMG], die verbazend moeilijk te navigeren is. Dit gezegd zijnde, is het natuurlijk altijd mogelijk een bestaande interface te nemen en uit te breiden. Zolang de methoden uit het basistype niet van "signature" veranderen, en hun gebruik ook niet verandert is er geen enkel probleem. Wat nog overblijft is het zogenaamde "fragile base class" probleem. Hier geeft CORBA geen antwoord op. 4.4.3 Management van Objecten Als een object wordt gecreëerd waarvan bepaalde diensten beschikbaar zijn over de CORBA, dan moeten die diensten (en een referentie naar het object in kwestie) bekend worden gemaakt aan een "implementation repository". Deze verbindt bepaalde interfaces met objecten die aangesproken kunnen worden om de diensten te leveren. Objecten moeten expliciet vernietigd worden. Daarvoor voorziet CORBA een aparte methode, maar het wordt aan de implementatie overgelaten om te bepalen wanneer een object moet vernietigd worden. Daarmee levert CORBA geen enkele dienst om de moeilijke taak van memory management in een gedistribueerde omgeving. 4.4.4 Persistence Er bestaat een "externalization service" om objecten te converteren naar een stroom gegevens en terug. Daarvan maakt de "persistence object service" gebruik om objecten van en naar een opslagmedium te transporteren. De "persistence service" zelf moet een aantal moeilijkheden overwinnen die eigen zijn aan objecten. Een object heeft een duidelijke identiteit. Het object opslaan en weer terughalen
mag dus niet resulteren in twee objecten die rondwaren binnen hetzelfde systeem. Ten tweede moet het web van referenties dat objecten gewoonlijk vormen kunnen worden bewaard en hersteld. Ten derde moet vermeden worden dat er met objecten kan worden geprutst terwijl ze opgeslagen zijn, om de principes van inkapsulatie te kunnen blijven respecteren [SZYP]. CORBA definieert "persistent objects", "persistent object managers", "persistent data services" en "persistent identifiers" om deze problemen aan te pakken. Toch bevatte de standaard enkele technische fouten, en het loste niet alle problemen (zoals geheugenallocatie) op [SZYP]. De standaard zal op korte termijn worden vervangen door de "Persistent State Service" [OMG]. 4.4.5 Interoperabiliteit Vanaf de CORBA 2.0 standaard is er een protocol voorzien om met andere CORBA 2.0 ORB's te communiceren via het IIOP, en er is ook zorg voor gedragen dat de standaard verenigbaar is met het Microsoft DCOM model [SZYP]. Doordat CORBA een zeer algemene standaard is, gespecifieerd op een abstract niveau, is het technisch mogelijk om een zeer breed scala van programmeertalen, besturingssystemen en hardware platforms met elkaar te verbinden. Langs de andere kant betekent dit wel dat er meer inspanning moet worden geleverd om een specifieke taal of systeem te verbinden met CORBA [SZYP]. Doordat CORBA geen binaire standaard is, kan het noodzakelijk zijn om gegevens te converteren bij het transport naar andere systemen of tussen ORB's. Dit kan dus een snelheidsverlies inhouden. 4.4.6 Huidig gebruik CORBA wordt dikwijls gebruikt in de industriële wereld, dikwijls als middleware om gedistribueerde systemen te realiseren [OMG][SZYP]. De "CORBAservices" worden daarbij maar selectief gebruikt, temeer omdat sommige nog steeds aan verandering onderhevig zijn. De "CORBAfacilities" bieden een framework aan voor bepaalde functionaliteit of voor bepaalde domein-specifieke kennis. OpenDoc is bijvoorbeeld geadopteerd als een framework voor documenten. Dit gebied van CORBA is nog in volle ontwikkeling en zal moeten gedragen worden door bedrijven uit de respectieve domeinen. Szyperski merkte op dat dit aspect van de OMG groep nog niet veel momentum had, en gebaseerd op de succesverhalen van de OMG site concludeer ik dat de situatie sinds 1997 niet veel veranderd is.
4.5 COM/DCOM/COM+ De COM familie is een standaard die geëvolueerd is uit VBX controls, OLE controls, en uiteindelijk door Microsoft tot een platform is gesmeed om componenten op een gedistribueerde manier te laten samenwerken. Daarbij is DCOM ("distributed COM") een uitbreiding die de bestaande concepten van "proxies" en "stubs" gebruikt om ook communicatie ove een netwerk mogelijk te maken. COM+ daarentegen is een lichtgewicht component-model, enkel bedoeld voor gebruik binnen een proces, dat abstract genoeg is om verschillende programmeertalen (zoals Java) toe te laten en dat diensten aanbiedt zoals introspectie en "garbage collection" [SZYP]. 4.5.1 Overerving en hergebruik Overerving van interfaces kan alleen enkelvoudig gebeuren. Langs de andere kant kan een enkel COM component wel verschillende interfaces implementeren. Hergebruik geschiedt op twee mogelijke manieren: een bestaand component kan simpelweg worden geincorporeerd in het nieuw component ("containment"), of het kan via een speciale manier worden geaggregeerd ("aggregation"). Bij "containment" implementeert het buitenste component zelf alle interfaces. Methodes voor bepaalde interfaces worden dan doorgegeven aan het binnenste component. Deze methode kan oneindig recursief worden toegepast. Maar als de kost van het oproepen van deze ketting erg groot wordt in verhouden tot de kost van het uitvoeren van de uiteindelijk code, wordt deze methode inefficiënt. De twee methode is typisch voor COM, en vereist dat het geaggregeerde component actief samenwerkt. In dat geval wordt de interface van het geaggregeerde component rechtstreeks opgeroepen, terwijl de twee componenten toch als een geheel naar buiten treden, en dus ook altijd de volledige set aangeboden interfaces wordt aangeboden. Daartoe geeft het geaggregeerde component altijd een referentie naar het aggregerende component terug. 4.5.2 Versioning Eens een bepaalde interface is gepubliceerd, wordt ze verbonden met een bepaalde unieke identifier. Nieuwe interfaces, of zelfs de vernieuwde versie van een bepaalde interface krijgen een volledig nieuw nummer. Op die manier kunnen verschillende versies
niet met elkaar verward worden. Het syntactische "fragile base class" probleem wordt effectief omzeild (de syntax van de interface met nummer xxx kan niet veranderen), maar voor de semantische kant wordt geen oplossing aangeboden [SZYP]. Om transitie naar een nieuwe interface eenvoudiger te maken, is het dus mogelijk om zowel de oude interface als de nieuwe interface te implementeren. Ik heb er geen technische uitleg over gevonden, maar het mechanisme van "dispatch tables" laat volgens mij zelfs toe dat, afhankelijk van de interface die wordt gebruikt, twee methodes met exact dezelfde naam en argumenten toch naar verschillende stukken code kunnen leiden. 4.5.3 Management van Objecten De basis-interface waarvan alle andere interfaces zijn afgeleid bevat twee methodes die dienen om het aantal referenties bij te houden, zodat een component kan worden gedealloceerd wanneer het niet meer wordt gerefereerd. In principe kan zelfs het aantal referenties per interface worden bijgehouden, zodat een object een deel van de resources kan vrijgeven als bepaalde diensten niet meer worden gebruikt. Het belangrijkste nadeel van deze werkwijze is dat het aantal referenties nooit op 0 komt als er een lus in de keten van objecten zit. Om dit te vermijden bestaan er vrij ingewikkelde protocols die ook erg foutgevoelig zijn [SZYP]. 4.5.4 Persistence Creatie van een object gebeurt in twee stappen. Ten eerste wordt het geheugen gereserveerd, en ten tweede moet het object worden geïnitialiseerd. Dit kan door elk COM object individueel worden opgelost. Er kunnen initialisatiefuncties bestaan, maar er kunnen ook initialisaties bestaan die de instellingen uit een file inlezen, om op die manier dus een object te creëren dat op harde schijf was opgeslagen [SZYP].. Bepaalde interfaces, allemaal afgeleid van IPersist, duiden aan dat een object kan geinitialiseerd worden vanuit files, datastromen, en andere. Er is een manier van opslag voorzien die "structured storage" noemt. Dit is een hiërarchisch formaat binnen een file [SZYP]. Het belangrijkste is wel dat als een object twee keer word ingelezen uit zo'n opslagplaats, dat er dan ook twee objecten ontstaan. Persistente opslag bewaart de identiteit van een object dus niet [SZYP]. 4.5.5 Interoperabiliteit
COM is gegroeid vanuit het wintel platform, en wordt ondersteund door de verschillende Microsoft applicaties. Bovendien zijn er ook inspanningen geweest om componenten met COM architectuur te laten draaien op andere systemen. 4.5.6 Huidig gebruik Met de steun van een gigant als Microsoft is het bijna gegarandeerd dat er een markt zal komen voor COM objecten. De eigen applicaties zijn al herschreven zodat zowel Office als de API van het besturingssysteem als COM server of client kan dienen. ActiveX objecten, COM objecten die aan bepaalde voorwaarden voldoen, worden al dikwijls gebruikt in de Internet Explorer. Visual Basic is aangepast zodat het ActiveX componenten kan gebruiken, en daarmee kan in Visual Basic nu ook een component in een component zitten.
4.6 JavaBeans De Javabeans specificatie van sun [JAVA] bevat de beschrijving van enkele klasses en interfaces, en een reeks richtlijnen wat een "goede" Javabean allemaal moet doen. In principe is elke klasse in Java een bean, maar zonder speciale voorzieningen is het dan wel eentje waar je niet zo veel mee kan aanvangen. Via introspectie kunnen development tools ontdekken hoe een Bean wordt gebruikt. Er zijn de publieke methodes, maar er zijn ook wat Sun "design patterns" noemt, al is het minder verwarrend om te spreken van "method patterns" [SZYP]. Via methodes die aan bepaalde patronen voldoen, kan een tool worden duidelijk gemaakt dat er publieke attributen bestaan die kunnen worden veranderd, en eventueel ook dat het mogelijk is om verwittigd te worden als de waarde ervan wordt veranderd, of zelfs dat het mogelijk is om verandering van de waarde tegen te gaan. Daarbij is het return-type, de naam, en de argumenten van de functies van belang. Als bijvoorbeeld de BeanBox een getter en setter zien met dezelfde naam, wordt zo'n patroon herkend, en komt het attribuut in de lijst met aanpasbare eigenschappen. De JavaBeans API voorziet ook nog speciale classes en interfaces om de tools te helpen met het vinden van eigenschappen, om extra informatie te geven over de bean in kwestie, om events te regelen, en classes om de bean aan te passen op een gebruiksvriendelijke (grafische) manier. Geen enkele van deze klassen is echter verplicht om een bean te maken. Er is dus ook geen basisklasse waarvan de bean absoluut moet afgeleid zijn.
4.6.1 Overerving en hergebruik Alle java mechanismen voor overerving en hergebruik staan ter beschikking van de bean ontwikkelaar. Met andere woorden: klassen kennen een enkelvoudige overerving, terwijl een klasse meer dan een interface mag implementeren en elke interface zelf subtype mag zijn van meerdere interfaces. Dit onderscheid tussen klasses en interfaces omzeilt enkele moeilijkheden die optreden bij een ruitvormig overervingsschema, maar langs de andere kant zijn de voordelen van meervoudige "interface inheritance" (Szyperski's term klopt hier perfect) wel mogelijk. De combinatie JDK 1.0 en bean specificatie 1.0 maakten het moeilijk om een enkele bean te laten bestaan uit meer dan een klasse. Het was zelfs moeilijk om het bean te laten vergezellen van resources zoals bitmaps of configuratiefiles op een eenduidige manier. Met de specificatie van de JAR file kunnen alle benodigdheden worden samengevoegd in een enkele file. Om toe te laten dat een bean uit meerdere klassen bestaat, is het wel niet meer wenselijk dat de ingebouwde java operatoren instanceof en de standaard "typecasting" worden gebruikt om beans te promoveren naar een ander type. Daarvoor worden twee functies, Beans.getInstanceOf() en Beans.isInstanceOf() geleverd. 4.6.2 Versioning Noch de Java specificatie noch de bean specificatie voorzien iets om verandering van interfaces bij te houden. Een subtype kan dus gewijzigd worden (syntactisch en/of semantisch), waardoor bestaande beans kunnen worden gebroken. Bij serialisation wordt wel een "serial version ID" meegegeven, een 64-bits code die afhangt van de naam van de klasse, en alle geïmplementeerde methoden die nieuw zijn gedefinieerd in die klasse. Deze code slaat alleen op de huidige klasse, en niet op de superklasse, die haar eigen code heeft. 4.6.3 Management van Objecten In de "Java virtual machine" wordt het geheugen beheerd door een volautomatische "garbage collector". Dit systeem is uitgebreid voor gedistribueerde systemen, zodat de programmeur zich bijna niks van geheugen management moet aantrekken. De constructor duidt het begin van een nieuw object aan, en de methode "finalize()" wordt aanroepen als de garbage collector beslist dat het object in kwestie mag weggegooid worden. Toch zijn er enkele problemen bij gedistribueerde uitvoering. RMI maakt gebruik van zogenaamde "proxy"-objecten als er een referentie bestaat naar een object aan de andere
kant van de lijn. Echter, als deze referentie de lijn twee keer passeert, (en de referentie intuitief terug een "echte" referentie is), wijst de referentie in werkelijkheid naar een proxy van een proxy. Ten tweede kan een thread zichzelf vastzetten in een "deadlock" via een omweggetje naar een ander systeem. Normaalgezien geldt dat een thread opnieuw mag synchroniseren op een object waar het al op gesynchroniseerd is. Maar via RMI wordt de identiteit van de originele thread niet behouden, en dus kan de thread opeens vastlopen terwijl het probeert te synchroniseren. 4.6.4 Persistence Persistentie is mogelijk door objecten te serialiseren en ze dan naar een file weg te schrijven. Als ze later terug worden gehaald van file, wordt het object plus het web van verwijzingen terug hersteld. Wegschrijven en meteen weer lezen van een cluster objecten resulteerd wel in een tweede cluster. Deze vorm van persistentie respecteert dus ook de identiteit van objecten niet. 4.6.5 Interoperabiliteit Een Java "virtual machine" is beschikbaar voor vrij veel systemen, en bovendien zit er eentje in de webbrowsers zoals Internet Explorer en Netscape Navigator. Een gedistribueerd systeem dat wil gebruik maken van de Java RMI is natuurlijk wel beperkt tot de taal Java. Langs de andere kant is er de Java IDL, een goed gedocumenteerde mapping van de Java datatypes naar de OMG IDL, en zijn alle CORBA datatypen beschikbaar via aparte Java packages. Java gecombineerd met CORBA heeft zowel de voordelen van de taal, als de krachtige object-georienteerde connectie met andere platformen en talen [LEWAN]. 4.6.6 Huidig gebruik Java heeft zich op korte tijd opgeworpen als taal voor internet-gerichte toepassingen. Het is nu het niveau van simpele applets ontstegen en definieert zich als een krachtige objectgeorienteerde taal met vele voorzieningen. Steeds meer bedrijven schakelen over op ontwikkeling in java. De krachtige voorzieningen voor internet verbindingen zijn daarbij alleen maar een troef. De combinatie Java en CORBA is daarbij een veelbelovend medium voor gedistribueerde verwerking of client/server toepassingen [LEWAN], dat al verschillende keren succesvol is toegepast [OMG].
5 Verlanglijstje
Component-georienteerd programmeren kent nog steeds enkele struikelblokken. Voor sommige kwesties bestaat al een mogelijke oplossing, maar wordt ze nog niet op grote schaal toegepast. In dit hoofdstuk komen enkele kwesties aan bod, en wordt een eventuele oplossing besproken.
5.1 Taal De procedurele, modulaire en object-georienteerde paradigma's kennen elk een aantal programmeertalen die specifiek ontworpen zijn met "hun" paradigma in het achterhoofd. De talen Pascal, Modula-2 en Oberon vormen zo een familie van talen. Voor het component-georienteerd paradigma is dit voorlopig nog niet het geval. Wel zijn er al een reeks richtlijnen over wat "goede" componenten doen, en er bestaan ook een aantal frameworks in een bestaande taal die als basis, maar ook als voorbeeld dienen voor een component-georienteerde structuur. Gadgets en Component Pascal zijn gedefinieerd als uitbreiding van Oberon [OBERON], JavaBeans als uitbreiding op Java. CORBA en COM+ zijn taalonafhankelijk gedefinieerd. Misschien is het wenselijk om een nieuwe taal te creeren, die typische componentgeorienteerde constructies in een formele syntax vat, net zoals eerder inkapsulatie en "type-checking" via de taal geformaliseerd werd. Wat volgt zijn een paar voorbeelden van constructies die meer naar voor komen in een component-georienteerd paradigma, en hoe een nieuwe taal daaraan tegemoet kan komen. Ik besef dat het creëren van een nieuwe taal geen sinecure is. Algemeen aanvaarde talen zoals C++ bevat genoeg "eigenaardigheidjes" die misschien niet gewenst waren. De taal Java bevat enkele vaagheden die door Szyperski worden aangestipt [SZYP], en in het ontwerp van CORBA, hoewel taal-onafhankelijk, zaten enkele technische fouten die de implementatie van bepaalde diensten zo goed als onmogelijk maakten. Dit maar om aan te tonen dat zelfs een team van experts niet noodzakelijk een perfecte taal aflevert, en ik heb dan ook niet de pretentie om beter dan hun te willen presteren. De volgende voorstellen zijn dan ook geen concrete ontwerpen voor een nieuwe taal, maar eerder uitgewerkte voorbeelden van vage ideeën.
5.1.1 Toevoegen van keywords Een van de belangrijkste dingen die me opvielen terwijl ik javabeans programmeerde, was de enorme hoeveelheid code die nodig was om vrij simpele algemene concepten te implementeren als publieke attributen, of events. Simpel in deze context wil zeggen dat het algemene idee is samen te vatten in een term of in enkele woorden, maar het was toch niet helemaal vanzelfsprekend om alles ook zo kort te formuleren in Java. Als voorbeeld beperk ik me hier tot wat Sun "attributes" van de beans noemt. Attributen zijn vergelijkbaar met publieke variabelen, behalve dat het bean een grote mate van controle behoudt over de eigelijke waarde van de variabele. Attributen kunnen worden ingesteld om het gedrag of het uiterlijk van een bean naar wens aan te passen, vooraleer het in te bouwen in een componenten systeem. Attributen kunnen "bound" zijn, wat betekent dat objecten kunnen worden op de hoogte gebracht van veranderingen in waarde. En ze kunnen "constrained" zijn, wat betekent dat bepaalde waarden kunnen geweigerd worden door degene die veranderingen in het oog houden. Opdat de beanbox het "bound" en "constrained" attribuut "MijnVar" van het type "MijnType" zou herkennen, moeten volgende methodes geimplementeerd worden: public public public public public public
void setMijnVar(MijnType MijnVar) throws PropertyVetoException; MijnType getMijnVar(); void addMijnVarListener(PropertyChangeListener pcl); void removeMijnVarListener(PropertyChangeListener pcl); void addMijnVarListener(VetoableChangeListener vcl); void removeMijnVarListener(VetoableChangeListener vcl);
Bovendien moeten in een klasse waar "bound" en "constrained" attributen voorkomen nog vier andere methoden worden geimplementeerd, opdat de Beanbox ze zou kunnen gebruiken: public public public public
void void void void
addVetoableChangeListener(VetoableChangeListener vcl); removeVetoableChangeListener(VetoableChangeListener vcl); addPropertyChangeListener(PropertyChangeListener pcl); removePropertyChangeListener(PropertyChangeListener pcl);
Alles tezamen vereist het eerste attribuut bijna een bladzijde aan code, zelfs al stijgt de hoeveelheid code waarschijnlijk niet lineair met het aantal attributen. De code is tamelijk simpel en voor verschillende attributen lijkt ze sterk op elkaar, maar toch is het moeilijk om de implementatie ervan aan een macro toe te vertrouwen, of om stukken code efficiënt te hergebruiken. Dit zorgt voor twee grote problemen. Zo'n hoeveelheid code is foutgevoelig, terwijl de fouten toch moeilijk kunnen worden opgevangen door statische controle. Een spellingsfout in de naam van een functie zal bijvoorbeeld geen syntactische fout opleveren, maar het zal er wel voor zorgen dat de beanbox het component niet ten volle kan gebruiken. Of omgekeerd, een onbedoelde verwarring van namen kan verkeerde acties op verkeerde momenten opleveren.
Ten tweede zorgt de hoeveelheid code ervoor dat de stap tussen een vrij simpel, atomisch idee ("ik heb zo'n attribuut nodig") en de implementatie ervan vrij groot is. Dit leidt de aandacht van de programmeur onnodig af. Een mogelijke manier om deze code explosie te vermijden, is om nieuwe keywords toe te voegen aan de taal. Een declaratie van ons voorbeeld-attribuut zou er dan zo kunnen uitzien: public bound constrained MijnType MijnVar;
De taal zou dan zonder tussenkomst van de programmeur moeten voorzien dat andere objecten verwittigd willen worden van veranderingen in het attribuut. De bean die het attribuut bevat moet controle kunnen uitoefenen op de waarde van het attribuut via bepaalde constructies. Sun gebruikt de notie van het "VetoableChangeEvent" om aan te duiden dat een attribuut van waarde gaat veranderen, tenzij dit tegengegaan wordt. Het "PropertyChangeEvent" wordt gegenereerd als het attribuut effectief van waarde is veranderd. Gebaseerd op deze termen zou een nieuwe taal volgende syntax kunnen voorzien. Voor ons attribuut "MijnVar" kan een bean al dan niet volgende methoden definieren: private void MijnVar::VetoableChange(MijnType o, MijnType n) throws Veto; private void MijnVar::PropertyChange(MijnType o, MijnType n); private void MijnVar::Calculate();
De methode Calculate() is erbij gevoegd om mogelijk te maken dat de effectieve berekening van het attribuut wordt uitgesteld totdat de waarde ervan wordt opgevraagd. Daar komt dus code die in de Javabeans specificatie typisch gezien in de getter functie zal staan, voor het return statement. Het belangrijkste voordeel is dat de taal veel minder lijnen code vereist om het typische geval te omschrijven. Het typische geval is een "bound" en "constrained" attribuut, dat geen speciale code vereist om nieuwe waarden van het attribuut te controleren. In dat geval is het mogelijk om de drie methoden "VetoableChange()", "PropertyChange()" en "Calculate()" niet te definieren. Een bijkomend voordeel is dat een compiler op deze manier meer aan "type-checking" kan doen dan in de javabeans standaard. Het is niet mogelijk een methode "VetoableChange()" te verbinden met een variabele die niet gedeclareerd is, het is ook niet mogelijk om de naam van de methode te misspellen, of om het type van de parameters verkeerd aan te geven. De compiler kan al deze fouten statisch detecteren. De syntax die ik hier voorstelde is verre van optimaal; het antwoordt bijvoorbeeld niet op de kwestie hoe andere beans op de hoogte kunnen worden gebracht van veranderingen in ons attribuut. Bij voorkeur zou dat gebeuren op een zeer gelijkaardige wijze. Het belangrijkste nadeel is wel dat elk keyword slechts een vrij specifieke oplossing biedt voor een vrij specifiek probleem. Om andere veel voorkomende constructies bij
componenten aan te duiden zijn weer andere keywords nodig. Dit zou kunnen leiden tot een moeilijk te leren en te lezen taal, en dat is natuurlijk helemaal niet de bedoeling. 5.1.2 Aspect-orientatie Door het concept van aspecten te introduceren in een programmeertaal, kan misschien vermeden worden dat de taal uitgebreid moet worden met stapels keywords. Een aspect is een vorm van hergebruik voor code die verspreid kan zijn over verschillende methoden, klassen, en ook modules [SKM]. Het idee is eerder al vernoemd in 2.2. Om terug te komen op het voorbeeld van een "bound" en "constrained" attribuut: een aspect kan definieren dat voor elk paar setters en getters een aantal extra variabelen bestaan, zoals de lijst met objecten die van veranderingen op de hoogte wil gebracht worden. Bovendien zou een aspect kunnen definieren dat voordat een setter wordt uitgevoerd, eerst de lijst moet worden afgelopen om toestemming te vragen aan alle objecten voor de verandering, terwijl na het uitvoeren van de setter de lijst wordt afgelopen van alle objecten die verwittigd wilden worden. De literatuur over aspect-georienteerd programmeren die ik gevonden heb [XEROX][SKM][AJ] liet me jammer genoeg een beetje op mijn honger zitten wat betreft het volledige toepassingsgebied van aspecten. Maar als aspecten hun belofte kunnen waarmaken, zouden ze wel eens het middel bij uitstek kunnen zijn om de repeterende code eigen aan componenten te vermijden. 5.1.3 Componenten als threads Component-georienteerd programmeren vertoont vele trekjes van multithreaded en gedistribueerd programmeren. Van javabeans wordt vereist dat ze threadsafe zijn. De COM architectuur is berekend op communicatie vanuit verschillende processen, terwijl DCOM communicatie vanop verschillende computers voorziet. Het CORBA object model is uiteraard volledig gedistribueerd opgevat. Gedistribueerd programmeren in het algemeen, maar ook in een object-georienteerde omgeving brengt enkele typische moeilijkheden met zich mee. Er zijn steeds weerkerende problemen, zoals synchronisatie, de mogelijkheid van deadlock, het probleem dat "glitches" binnen een object van buitenaf niet merkbaar mogen zijn, enzoverder. In deze context leek het mij niet onlogisch om een model te hanteren waar elk object in principe een thread bevat. Op pagina 956 van [OOSC2] en verder legt Bertrand Meyer uit waarom de link tussen threads en objecten zo eenvoudig gelegd is: beide genieten een vorm van zelfstandigheid en inkapsulatie. Beide kennen een interne "toestand" die acties kan bepalen. Voor beide is het een goed idee om interactie sterk aan banden te leggen en
te reguleren. Communicatie mechanismen voor beiden wordt wel eens "messages" genoemd [OOSC2]. Simula 67 kent geen volledig concurrent programmeermodel, maar het ondersteunt wel de notie van een "actief object". Naast een stel methodes en variabelen kan een klasse ook een "body" bevatten, een reeks instructies die wordt uitgevoerd als een co-routine, oftewel een manier om semi-parallellisme te bereiken met een enkele thread. Deze body kan geïnterpreteerd worden als een constructor, die normaalgezien alleen een object initialiseert, maar zich eventueel ook kan loskoppelen ("detach") om daarna karakteristieken van een aparte thread te vertonen [OOSC2]. Het belangrijkste nadeel van actieve objecten wordt duidelijk als een nieuw subtype moet worden afgeleid van een bestaand actief object [OOSC2]. Moet dit nieuw type twee threads herbergen (de oude, en een nieuw gedefinieerde)? Indien niet, moet de thread van het basistype dan weggegooid worden en moet de volledige werking opnieuw gespecifieerd worden? Dit lijkt in strijd met het principe van inheritance. Simula 67 biedt een tussenoplossing. Het is mogelijk in de "body" van de superklasse om te specifieren waar de code van een eventueel subtype moet terecht komen. Meyer merkt op dat dit een aantal object-georienteerde principes schendt, omdat de implementatie van het subtype op die manier sterk is verweven met de implementatie van het supertype. Het is nodig om overerving tot in de kleinste details te plannen om zoiets mogelijk te maken, en dat strookt niet met het idee van inkapsulatie [OOSC2]. Hier raken we ook aan de gevaren van wat Szyperski "implementation inheritance" noemt [SZYP]. Meyer werpt ook nog een tweede bezwaar op. Als alle objecten actief zijn, dan vereist het uitwisselen van boodschappen een zeer sterke synchronisatie. Bij actieve objecten komt een boodschap neer op het uitvoeren van een functie. Functieparameters zijn daarbij de binnenkomende gegevens, het return type de uitgaande informatie. Omdat het ene thread wacht op informatie van de andere thread, ligt deze tijdelijk stil. Tegelijk kan een derde object met geen van beide communiceren, omdat beide threads bezig zijn met uitwisseling van informatie onderling. Op die manier worden bijna alle voordelen van concurrentie teniet gedaan [OOSC2]. Mijns insziens kunnen een deel van deze synchronizatie problemen vermeden worden als wordt afgestapt van de notie van een functie met returnwaarde. In het voorbeeld van communicerende objecten zouden de meeste threads 90% van de tijd inactief zijn en wachten op informatie uit andere objecten. Agha's "actors" model met asynchrone boodschappen en gebufferde verwerking kan hier uitkomst bieden. Toch is het model nooit op grote, commerciele schaal toegepast. COM biedt nu wel het principe van de "non-blocking method call", waarbij een thread verder kan lopen terwijl het op een returnwaarde van een aangeroepen functie wacht [NBMC]. Maar de belangrijkste reden om af te stappen van het idee van actieve objecten is veel simpeler: het idee is tamelijk gelimiteerd, want het biedt slechts 1 thread per object, en het heeft in mindere of meerdere mate bovenstaande voordelen. En in ruil voor deze nadelen biedt het geen belangrijke voordelen. Integendeel: in andere talen zoals Java is het
mogelijk om een actief object te simuleren zonder veel "overhead" in de code. En de "nonblocking method call" uit COM is perfect bruikbaar in een gedistribueerd systeem zonder de notie van een actief object. Conclusie: voor individuele objecten of componenten kan het nuttig zijn intern een eigen thread te gebruiken, maar om deze constructie in de syntax van een taal vast te leggen is te restrictief.
5.2 Contract De enige manier om een component te kunnen gebruiken zonder de code te moeten doornemen is op basis van een formele specificatie, oftewel een contract. De aangeboden functie-"signatures" zijn zeker een deel van dit contract, maar meestal zal dit niet voldoende zijn om een component opnieuw te kunnen gebruiken met vertrouwen.
5.2.1 Pre- en postcondities Een manier om op een formele wijze extra informatie te geven over het effect van een functie wordt gedemonstreerd in Eiffel. Voor elke functie is het mogelijk pre- en postcondities op te geven, en op niveau van de klasse kunnen ook invariants worden gespecifieerd [OOSC2]. Omdat contracten in Eiffel simpelweg in de programmeertaal zelf worden geforumuleerd, kan zeer veel uitgedrukt worden op een formele wijze, die bovendien automatisch (bv. door de compiler) kan worden nagekeken. Betrand Meyer geeft natuurlijk hoog op van de voordelen van het Design-by-Contract principe [ARIANE]. Op de boude stelling dat Eiffel de crash van de Ariane had kunnen voorkomen, is tamelijk wat reactie gekomen. Ondanks de algemene toon in zijn artikel, weet Ken Garlington toch enkele gegronde bezwaren op te werpen. In de grond komt alles neer op het feit dat de software-modules voor de Ariane nooit tezamen zijn getest. De dynamische controle van Eiffel had alleen kunnen doen wat het Ada systeem ook gedaan heeft: een exceptie genereren. En de statische controle (de enige mogelijkheid bij aparte compilatie) evalueert geen contracten bij mijn weten. Het enige voordeel van Eiffel in dit geval had kunnen liggen in het feit dat de contracten een vorm van documentatie zijn die hoogstwaarschijnlijk beter "up-to-date" wordt gehouden. Ik zou het fenomeen pre-conditie willen opsplitsen in twee soorten: voorwaarden die gelden voor de klasse waarvan een methode wordt opgeroepen (maw: het object moet zich in een bepaalde toestand bevinden), en voorwaarden die gelden voor de binnenkomende parameters.
Voorwaarden op het object (of de klasse) zelf geven een indicatie over het juist gebruik van de klasse. Door ze te lezen kan een programmeur zich een beeld vormen over de juiste volgorde van functie-aanroepen. Als de pre-condities op run-time worden geevalueerd, dan zal een exceptie altijd wijzen op een fout in de software [OOSC2]. Voorwaarden die gelden voor de binnenkomende parameters kunnen dezelfde rol vervullen als de andere pre-condities, maar er is nog een extra dimensie waarvan Eiffel geen gebruik maakt. Deze parameters, tezamen met hun voorwaarden kunnen worden gezien als aparte datatypes, subtypes van het "gewone" parametertype. "Alle integers groter dan 5" is bijvoorbeeld een subtype van integer. Voor een bepaalde expressie die aan een methode wordt gegeven, zou op een statische manier kunnen gecontroleerd worden of een gevormde expressie daadwerkelijk een subtype is van het argument-type (met constraints!). De compiler (of linker) zal dan een foutmelding geven als aan een methode een parameter wordt doorgegeven waarvan statisch niet gegarandeerd wordt dat de expressie voldoet aan de pre-condities. Zulke controles zijn niet niet altijd even makkelijk te implementeren. De grootste problemen situeren zich rond abstracte datatypes van de programmeur, die als argument aan een methode worden gegeven. Je kan niet verwachten dat een compiler begrijpt dat ADT.isGroterDanVijf() een strengere voorwaarde is dan ADT.isGroterDanVier(). Bovendien zou een subtype de semantiek van bepaalde methodes volledig kunnen veranderen. Op die manier zou het subtype wel nog kunnen beantwoorden aan de voorwaarden door de compiler gesteld, maar zou het eigelijk totaal ongeschikt kunnen zijn om als argument van onze methode te dienen. Ondanks deze problemen denk ik dat het jammer is dat Eiffel geen onderscheid maakt tussen de twee soorten pre-condities. Pre- en postcondities zijn bovendien ook niet de eindoplossing. Zo toons Szyperski aan dat het, via het Observer pattern, mogelijk is om de postcondities te breken [SZYP]. Bovendien moet een klasse die Observers toestaat, ook zeker weten dat de klasseinvarianten nog zullen gelden. In Eiffel bijvoorbeeld moeten de klasse-invarianten alleen gelden vlak voordat een methode wordt aangeroepen, en vlak nadat de methode terugkeert [OOSC2]. Als tijdens de uitvoer van die functie dus Observers worden geactiveerd, is het theoretisch mogelijk dat ze een object zullen aantreffen waarvan de klasse-invarianten niet gelden. Als de "run-time" controle actief is, zal er natuurlijk wel een exceptie gegenereerd worden zodra het "onstabiele" object wordt aangesproken. 5.2.2 Niet-functionele vereisten Naast vereisten in verband met de pure functionaliteit, worden aan herbruikbare componenten dikwijls ook nog andere eisen gesteld, zoals efficiëntie, betrouwbaarheid, gebruiksvriendelijkheid, robuustheid, en dergelijke. Dergelijke eigenschappen zijn al moeilijk genoeg om informeel te omschrijven. In het licht van hoofdstuk drie (de verschillende soorten kwaliteit) is het nochtans duidelijk dat systemen die een zekere
garantie willen bieden op gebied van niet-functionele kwaliteit, overeenkomstige garanties zouden moeten kunnen eisen van de componenten waaruit ze bestaan. Het doet er niet toe of het systeem nu dynamisch via een "object request broker" wordt samengesteld, of het wordt allemaal netjes statisch ingepakt door de ontwikkelaar. Van zodra gezocht wordt naar componenten buitenshuis, moet er op het prijskaartje ook informatie staan over de eigenschappen van een component. Voorlopig is dit zelden het geval. Commerciële software wordt dikwijls verkocht zonder de minste garantie; zelfs geen garantie over de functionaliteit! 5.2.3 Wettelijke aansprakelijkheid Bij het woord "contract" komt meteen ook het equivalent buiten de software wereld in gedachten. Daar heeft een contract bindingskracht, en wie het contract breekt is strafbaar. Gebruik van externe componenten kan een pak aantrekkelijker worden als de externe ontwikkelaar ook daadwerkelijk verantwoordelijk kan worden gesteld voor fouten in het aangekochte component. Deze materie is echter zeer complex. In de staat Texas wordt nu overwogen om de term "software engineer" te verbinden aan een wettelijke licentie [TEXAS]. Enkele van de vragen die daarbij opkomen is wat er gebeurt voor de huidige generatie programmeurs: moeten zij een examen afleggen, opnieuw les volgen, ... ? Een andere, maar enigzins vergelijkbare vraag is wat er moet gebeuren met legacy systemen. Wie is verantwoordelijk als daar iets misloopt? Een andere kwestie is de wisselwerking tussen contract en vergoeding [PRIMER].
6 Magic: the Gathering
6.1 Het spel MtG is een "collectible cards game". Dat wil zeggen dat iedereen een eigen verzameling kaarten heeft. Twee spelers die tegen elkaar wensen te spelen, stellen een "deck" samen uit hun verzameling kaarten, typisch tussen de 40 en 80 kaarten. Uit ieder deck wordt een kaart getrokken: de winnaar zal deze twee kaarten meenemen naar huis. Daarna worden in een beurten-systeem kaarten gebruikt, elk volgens regels de specifiek voor elke individuele kaart gelden. De kunst is om een goed "deck" samen te stellen, en de kaarten zo te combineren dat een maximum effect wordt bereikt.
De regels zijn uitgebreid, en er zijn pagina's en pagina's wedstrijdreglementen om interpretatie-verschillen bij 'officiële' wedstrijden te voorkomen. En zoals gezegd kan bovendien elke kaart nog volledig eigen instructies bevatten, die afgedrukt staan op de kaart zelf. Elke beurt kent een aantal fasen. Sommige acties (zoals het in het spel brengen van een kaart uit de hand) kunnen alleen in een bepaalde fase van het spel uitgevoerd worden. Andere kaarten kunnen dan weer alleen gespeeld worden als reactie op een andere kaart. De volledige regels kunnen afgehaald worden op mijn homesite [HOME]. De file is in het Windows .HLP formaat. Bepaalde concepten, zoals de verschillende categorieën van kaarten, zijn noodzakelijk om de klasse-structuur van het project te volgen, maar zijn niet van primair belang om de opmerkingen in 6.2 te lezen. Vandaar de beperkte uitleg van de regels in dit hoofdstuk.
6.2 De software De belangrijkste reden waarom ik dit spel heb gekozen als project, is juist het schier oneindig aantal mogelijkheden, en de grote lijst met kaarten die er voor het spel bestaan. Bij component-georienteerd programmeren is een van de hoekstenen dat componenten kunnen worden gebruikt in totaal nieuwe, mogelijk onvoorziene manieren. Een element van Magic is dat kaarten in een bijna oneindig aantal combinaties kunnen worden gebruikt, waarbij de effecten van 1 kaart worden veranderd, tenietgedaan of juist versterkt door andere kaarten. Ik heb dus gekozen om een enkele kaart voor te stellen als een component. Daarnaast bestaan er nog enkele componenten die het spelverloop regelen, en componenten die de speler voorstellen, omdat beide spelers deel zijn van het spel en kunnen worden uitgekozen als doelwit van sommige effecten. Er zijn ook enkele hulp-componenten om bepaalde aspecten van het spel voor te stellen, zoals iMagicColor, die het type "mana" aangeeft, of iMagicCost, dat aangeeft hoeveel "mana" nodig is om een bepaald effect (iMagicEffect) te activeren.
6.3 Code hergebruik Vele kaarten hebben een vergelijkbare basis-functionaliteit, en meestal verschillen ze slechts van elkaar via enkele speciale effecten. De gemeenschappelijke eigenschappen werden bijeengebracht in basis-types. Een kaart van een bepaalde categorie implementeerd gewoon de interface die bij de categorie hoort (iMagicSpell, iMagicLand, iMagicFastEffect, ...). De verschillende "interfaces" zijn verbonden via een "inheritance tree". Dit is wat Szyperski "interface inheritance" noemt (vrij toepasselijk in Java).
Om nog minder code te moeten schrijven, is de basis-functionaliteit ook al geimplementeerd in de klasse MagicBasicCard. Deze behandelt gemeenschappelijke dingen, die alle kaarten moeten aankunnen. Een kaart moet bijvoorbeeld kunnen toegevoegd worden aan een deck. Een kaart moet kunnen worden in de hand genomen worden, of op tafel gelegd. Elke kaart heeft een beschrijving, en alle kaarten belanden uiteindelijk op het kerkhof. Een eigen kaart programmeren begint dan met het overerven van de klasse MagicBasicCard, hoewel dit niet verplicht is. Dit is wat Szyperski "implementation inheritance" noemt.
6.4 Combineren van kaarten Hoe de kaarten (en dus de componenten) worden gecombineerd, kan best worden uitgelegd aan de hand van een voorbeeld. In dit voorbeeld ben ik aan de beurt, en ik heb enkele kaarten in het spel (dwz op tafel): het eerste is een "Hill Giant". Dit is een wezen dat de andere speler kan aanvallen, en op die manier 3 punten van het levenstotaal van mijn tegenstander kan om zeep helpen. Maar ik zie dat mijn tegenstander zelf een "wall of fire" heeft. Dit zijn ook wezens, maar deze kunnen niet aanvallen, alleen verdedigen. Als ik zou aanvallen met mijn reus, zal mijn tegenstander de reus stoppen met de "wall of fire". Ten eerste vermijdt die daardoor het verlies van drie levenspunten, en ten tweede kan hij op deze manier mijn reus doden. Dat risico wil ik dus niet lopen. Daarom wend ik mij tot de tweede kaart in het spel: "Ali Baba". Als u het sprookje kent, weet u dat Ali Baba muren op magische wijze kan openen. Het component "Ali Baba" heeft een speciale functionaliteit, die ik nu wens te activeren door op de juiste knop te klikken. De speciale gave vereist wel een rode mana. De uitvoering van het component wacht nu tot ik 1 rode mana activeer. Dat doe ik door op een "Mountain" te klikken. Dit is een kaart die als enig doel heeft rode mana op te wekken. Nu wordt de uitvoering van "Ali Baba" verder gezet. Ik mag een "wall" van de vijand aanduiden, die daarna onbruikbaar wordt voor de rest van mijn beurt. Ik klik dus op de "wall of fire". Nu is de weg vrij gemaakt voor mijn reus. Ik val aan, en er is geen enkele tegenstand, omdat mijn tegenspeler geen enkel ander wezen tot zijn of haar beschikking heeft. De reus kan dus rechtstreeks de speler aanvallen, en mijn tegenspeler is drie levenspunten zwakker. Maar vlak voordat de eigelijke aanval begint, speel ik nog een speciale kaart: ik heb in mijn hand nog de kaart "bloodlust" zitten. Om deze kaart te spelen heb ik twee mana nodig, dus ik klik op nog twee "Mountain" kaarten die al op de tafel liggen. Bloodlust vereist een bepaald doel, en ik klik nu op mijn reus. Gevolg: opeens is mijn reus een veel krachtiger wezen, en hij kan nu 7 levenspunten om zeep helpen in plaats van 3. Dat was de Magic kant van het spel. Nu iets technischer.
De "Mountain" kaarten, "Ali Baba" en de "Hill Giant" zijn componenten die zich in de container "speelveld" bevinden. Ze hebben een visuele representatie voor mijzelf, maar ook voor de tegenstander. De kaarten zoals "Bloodlust" die zich nog in mijn handen bevinden, behoren toe aan het component "hand". Ze hebben alleen een visuele representatie als de hand deel uitmaakt van de speler die op dit moment de computer bestuurt (in deze versie is dat altijd, omdat de applicatie niet gedistribueerd loopt). Elke kaart bevat ook een aantal knoppen, die overeenkomen met acties die mogelijk zijn met de kaart. Het speciale effect van Ali Baba activeren is dus gelijk aan klikken op de knop "tap target wall". Vervolgens spreekt Ali Baba het mana-reservoir aan. Er is nul mana voorhanden. Ali Baba registreert een "ManaListener" bij het reservoir, zodat het speciale effect kan worden verdergezet van zodra er mana voorhanden is. Ik, de gebruiker, weet dat de uitvoering is stopgezet vanwege de mana. Ik klik dus op de knop "tap" van een kaart "Mountain". Dit component creert een object rode mana, en stopt dit in het reservoir met de methode "addMana()". De verhoging van het beschikbare mana zorgt ervoor dat Ali Baba wordt gereactiveerd via de Listener. Het mana object wordt uit het reservoir gehaald met de methode "getMana()", en nu verwacht Ali Baba een klik op een kaart: namelijk de muur die moet worden geopend. Daartoe registreert Ali Baba zich bij iPlayField om muisklikken op te vangen. Als er geklikt wordt op een object, kijkt Ali Baba of het kan worden gecast naar een "Wall". Indien ja, dan wordt van de Wall de methode "tap()" opgeroepen. Gevolg: de "wall" is onbruikbaar voor deze beurt. Nu klik ik op de fase aanvallen. De "Hill Giant", die zichzelf als Listener geregistreerd had bij het fase-verloop object, weet dat hij nu kan aanvallen. Als ik op de juiste button klik, registreert hij zichzelf bij het aanval-component. Vervolgens mag mijn tegenstander klikken op kaarten om de aanval te blokkeren. In dit geval zou klikken op de "wall of fire" een foutmelding tot gevolg hebben: "wall of fire" controleert de eigen toestand, en ziet dat het "tapped" is. Nu klik ik op de fase aanval uitvoeren. Alle objecten die gingen aanvallen hebben hierop geregistreerd, en worden dus geactiveerd. De "Hill Giant" creert een iMagicDamage object van 3. Dat wordt verhoogt tot 7 door de "BloodLust" kaart, die geregistreerd is om alle iMagicDamage objecten van de "Hill Giant" te onderscheppen. Vervolgens wordt het object aan de speler gegeven, via iMagicPlayer.dealDamage(iMagicDamage x). Het puntentotaal gaat omlaag. Alles gebeurt via listeners. Op alle mogelijke gebeurtenissen kan worden gereageerd door op voorhand listeners te registreren. Dit betekent wel dat er alleen op activiteiten kan worden gereageerd wanneer reactie effectief is voorzien. Maar omdat er slechts een eindig aantal soorten gebeurtenissen is, is het mogelijk om alles te voorzien. Toch is deze lijst zeer groot.
6.5 Een alternatieve methode Zoals eerder gezegd leunt de bouw van kaarten op "interface-" en "implementation inheritance". Om een volledig nieuwe kaart te implementeren, is het nodig om een nieuwe klasse te definiëren en ze te compileren. Daarna kan ze worden ingevoerd in het systeem, en zal ze interageren met andere kaarten. Deze interactie is sterk muis-gebaseerd en doordat kaarten met elkaar interageren, vertoont het systeem kenmerken van componenten: er is speciale inspanning geleverd om de samenstelbaarheid te verhogen. In plaats van overerving had ik ook op compositie kunnen steunen voor de creatie van nieuwe kaarten. Ik had dan een basis kaart kunnen definiëren, bijvoorbeeld "MagicCreature". Een nieuwe kaart aanmaken zou dan beginnen met het creëren van een nieuw object MagicCreature. Door ook een object MagicDamage te creeren, zou ik deze twee kunnen verbinden. Ik heb nu vastgelegd hoeveel schade mijn nieuwe wezen kan toebrengen. Weer via de creatie van een object MagicDamaga kan ik vastleggen hoeveel schade mijn wezen kan oplopen voordat het zelf sterft. Door een verandering van een text-veld kan ik de naam van mijn creature vastleggen als "Goblin King". Op die manier gebeurt het "programmeren" van een compleet nieuwe kaart dus op een grafische manier, in plaats van door nieuwe code te schrijven. Op die manier vervaagt de grens tussen het programmeren van nieuwe kaarten, en het spelen van een Magic spel. Zowel samenstelbaarheid als gebruiksvriendelijkheid zijn veel groter dan het systeem dat ik nu gebruik. Maar ook de inspanning op gebied van code is veel groter. Had ik er eerder aan gedacht, dan zou ik de alternatieve manier hebben gekozen als basis voor mijn systeem. Jammer genoeg kwam dit slechts bij me op toen ik al vrij ver gevorderd was met design en implementatie. Dit maar om aan te tonen dat er verschillende soorten, maar ook verschillende "graden" van component-georienteerd programmeren zijn: ik zou de alternatieve manier als "meer" component-georienteerd willen bestempelen. De twee methoden contrasteren twee manieren waarop de componenten worden samengesteld, en waarop ze worden gecreëerd. Deze keuze wordt op geen enkele manier gesteund of ontmoedigd door de taal, maar toch is het een vrij fundamentele keuze, die veel van de mogelijkheden achteraf bepaalt. Naast zuivere programmeertechniek (taal en tools), is de manier van samenstellen dus ook zeer belangrijk. Dit is een aspect waar zeer vele werken (deze thesis incluis) alleen impliciet of terloops op wijzen, op uitzondering van [GENEVE].
7 Conclusie
Component-georienteerd programmeren gebruikt dezelfde methodes als het objectgeorienteerd paradigma, om dezelfde doelen te realiseren. De grens tussen objectorientatie en component-orientatie is dan ook geen scherpe lijn. Over het algemeen ligt het grootste verschil in de doelstelling: naast functionaliteit zijn herbruikbaarheid en samenstelbaarheid expliciet onderdeel van de doelstellingen die geformuleerd worden bij het ontwerp van een component. Doordat eigenschappen als herbruikbaarheid en samenstelbaarheid een deel van de goal worden, lijkt het erop dat component-georienteerd programmeren op dat gebied meer beloften zal waarmaken dan object-georienteerd programmeren. Het gevolg daarvan is een mogelijke "markt" voor componenten, maar die mogelijkheid brengt dan weer specifieke eisen met zich mee. Al met al zou ik het component-georienteerd paradigma een "heroriëntering" willen noemen van de object-georienteerde principes. Toch is de heroriëntering mijns inziens fundamenteel genoeg om een nieuwe term te gebruiken, dankzij introductie van concepten zoals "object request brokers", de vervaging van run-time en design-time, persistentie, enzoverder.
8 Referenties
[AJ] http://aspectj.org
[AGHA] "ACTORS: A Model of Concurrent Computation in Distributed Systems" Gul Agha MIT press, Cambridge 1986 [ALEX] Notes on the Synthesis of Form, Harvard University Press, 1964 The Oregon Experiment, Oxford University Press, 1975 A Pattern Language: Towns, Buildings, Construction, Oxford University Press, 1977 The Timeless Way of Building, Oxford University Press, 1979 [ARIANE] "Design By Contract: The Lessons of Ariane" Betrand Meyer, Jean-Marc "Jezequel" Computer (IEEE), Object Technology Department, vol 30, no. 1, January 1997 http://www.eiffel.com/doc/manuals/technology/contract/ariane/page.html
[CRITIC] http://www.flash.net/~kennieg/ariane.html
Ken Garlington [D] http://www.parc.xerox.com/csl/groups/sda/publications/papers/PARC-AOP-D97/forweb.pdf
[DIV] "Diversity in Reuse Processes" Maurizio Morisio, Colin Tully, Michel Ezran IEEE Software, July/August 2000 [EIF] http://www.eiffel.com
[FUND] "Fundamentals of Software Engineering" Carlo Ghezzi, Mehdi Jazayeri, Dino Mandrioli Prentice Hall 1991, ISBN 0-13-820432-2 [GENEVE] "Component-Oriented Software Development" Oscar Nierstrasz, Simon Gibbs, Dennis Tsichritzis Communications of the ACM, vol 35, no. 9, Sept 1992 [GO4] "Design Patterns: Elements of Reusable Object-Oriented Software" Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides Addison-Wesley Professional Computing Series 1995, ISBN 0-201-63361-2 [HILLSIDE] http://www.hillside.net/patterns/about/
[HOME] http://users.pandora.be/bart.huylebroeck1/
[IDIOM] "Advanced C++ Programming Styles and Idioms" James O. Coplien Addison-Wesley, 1992 ISBN: 0201548550 [INTRO] http://www.enteract.com/~bradapp/docs/patterns-intro.html
[IMP] "Software Reuse, Architecture, Process and Organization for Business Success" Addison-Wesley 1997, ISBN 0-201-92476-5 Ivar Jacobson, Martin Griss, Patrik Jonsson
[IPAL] "Idioms and Patters as Architectural Literature" James O. Coplien IEEE Software, January 2000 [JAVA] http://java.sun.com/products/javabeans/index.html
[LEWAN] "Frameworks for Component-Based Client/Server Computing" Scott M. Lewandowski ACM Computing Surveys, Vol. 30, No. 1, March 1998 [MSDN1] http://msdn.microsoft.com/library/default.asp?url=/library/enus/dncomg/html/msdn_components.asp?frame=true&hidetoc=true
[MYTH] "Patterns: The Top Ten Misconceptions" John Vlissides Object Magazine, March 1997 http://researchweb.watson.ibm.com/designpatterns/pubs/top10misc.html
[NBMC] http://msdn.microsoft.com/library/default.asp?url=/library/enus/dncomg/html/nbmc.asp
[OBERON] J. Gutknecht and M. Franz; "Oberon with Gadgets: A Simple Component Framework"; in M. Fayad, D. Schmidt, R. Johnson (Eds.) Implementing Application Frameworks: Object-Oriented Frameworks at Work Wiley, ISBN 0-4712-5201-8; September 1999. (A precursor to this article was published as UCI ICS Technical Report No. 96-55.) http://www.ics.uci.edu/~franz/pubs-pdf/BC03.pdf [OMG] http://www.omg.org http://www.corba.org http://www.corba.org/success.htm
[OOPUF] "Object-Oriented Programming, a Unified Foundation" Giuseppe Castagna Birkhäuser 1997, ISBN 0-8176-3905-5 / 3-7643-3905-5 [OOSC2] "Object-Oriented Software Composition, 2nd edition" Betrand Meyer: (p 956 ev)
Prentice Hall 1997, ISBN 0-13-629155-4 [PRIMER] "Software Licensing: A Primer" Andrea Chavez, Catherine Tornabene, Gio Wiederhold IEEE Software, September/October 1998 [SDM] "Column: Beyond Objects" Clemens Szyperski, Betrand Meyer, Bruce Powel Douglass, Cris Kobryn, Grady Booch, Jeff Scanlon Software Development Magazine, Okt 1999 - Juli 2001 [SGOF] "A System of Patterns, Pattern-Oriented Software Architecture" Frank Buschmann, Regine Meunier, Hans Rohnert, Peter Sommerlad, Michael Stal John Wiley & Sons 1996, ISBN 0-471-95869-7 [SKM] "Aspect-Oriented Programming Takes Aim at Software Complexity" Sandra Kay Miller Computer Magazine, April 2000 http://computer.org
[SZYP] "Component Software, Beyond Object-Oriented Programming" Clemens Szyperski Addison-Wesley 1997, ISBN 0-201-17888-5 [TEXAS] "Texas Poised to License Professional Engineers in Software Engineering" Donald J. Bagert Software Engineering Notes (ACM Press) vol 23, no. 3, May 1998 [THEO] "A Theory of Objects" Martin Abaldi, Luca Cardelli Springer-Verlag 1996,0-387-94775-2 [WIKI] http://c2.com/cgi/wiki?PortlandPatternRepository
[XEROX] http://www.parc.xerox.com/csl/projects/aop/