- David Keppel
Er blijkt echter relatief weinig onderzoek te zijn gedaan of, en waarom, op een OO-manier software ontwikkelen beter zou zijn dan andere bestaande manieren. Beweringen over dat het beter is hoort men alom, maar men beargumenteert dit dan vaak alleen met behulp van speelgoedvoorbeelden of abstracties waarvan niet aangetoond wordt dat deze in de praktijk reëel zijn. De psychologische aspecten van het OO ontwikkelproces worden vaak weinig expliciet behandeld.
We zijn nu in het stadium dat we OO als toegepaste techniek als volwassen
kunnen beschouwen. Men begint nu een aantal problemen die er zijn te
onderkennen, alhoewel de meeste populaire verhandelingen daar merkwaardig
genoeg nog steeds weinig aandacht aan besteden. Men kan nu spreken van van
vorige generaties overgeleverde OO software, en het blijkt nu, dat deze niet
minder problematisch te onderhouden is dan andere software [Smith et al 95]
[Daly et al 95]. Het is daarom misschien interessant om te onderzoeken waar
nu de problemen liggen. In dit verslag zal iets worden beschreven over de
taken waar men in grote projecten vaak voor komen te staan, en hoe het
object-georienteerde paradigma hier aan tegemoetkomt of tekortschiet. Dit zal
vooral bekeken worden in vergelijking tot andere procedurele paradigma's,
waarvoor OO vaak als vervanging gezien wordt.
1.2 Wat is software?
Voordat we ons gaan verdiepen in softwareontwikkeling en haar psychologie
zullen we eerst proberen om een algemeen beeld te vormen van hoe men tegen
software aan kan kijken. Op het niveau van de machine heeft men het programma,
een reeks instructies die een reeks manipulaties op het interne geheugen van de
machine uitvoeren, of keuzes maken aan de hand van de toestand van het geheugen
over welke manipulaties er vervolgens uitgevoerd moeten worden. De
begincondities van een programma of subprogramma zijn in het algemeen steeds
anders. Daarnaast is het nog mogelijk dat de toestand van het geheugen 'van
buitenaf' veranderd kan worden, bijvoorbeeld door invoer van een gebruiker, of
door een ander programma dat tegelijk draait. Het gedrag van een programma is
in het algemeen moeilijk te begrijpen omdat er meestal sprake is van een lange
reeks manipulaties waarvan de precieze uitkomst heel belangrijk kan zijn voor
het gedrag van de rest van het programma, terwijl er een grote hoeveelheid
mogelijke begincondities is waar het programma mee te maken kan, en zal,
krijgen [Pair 90]. De mogelijkheid van manipulaties buiten het programma om
maakt de zaak nog ingewikkelder, omdat men op elk moment tijdens het uitvoeren
van het programma daar rekening mee moet houden. Daarom wordt de communicatie
tussen programma's vaak zo minimaal mogelijk gehouden, zodat de meeste
programma's als bijna losstaand beschouwd kunnen worden.
De opzet van een programma kan uitgedrukt worden in een taal, hetzij een schema
of een document dat bedoeld is om een idee te krijgen van hoe het eruit ziet of
moet zien, hetzij een programmeertaal, die direct leesbaar en uitvoerbaar is
door de computer. Veel programmeertalen (de imperatieve talen) reflecteren op
een directe manier de structuur van de taal van de machine: een representatie
van een databewerking in de taal kan één-op-één
vertaald worden naar een reeks machine-instructies. De programmeertalen die
wij hier zullen bekijken zijn voor het grootste deel imperatief. Echter, zowel
de programmeer- als de beschrijvingstalen kunnen de nadruk leggen op
verschillende aspecten: met name onderscheidt men de control flow
(de volgorde waarop operaties worden uitgevoerd-een voorbeeld hiervan is een
stroomschema) en de data flow (de herkomst en bestemming van de
gegevens die door de operaties wordt bewerkt-een voorbeeld hiervan is een data
flow diagram).
1.3 Cognitieve complexiteit
Complexiteit is een vaak genoemde term in software-engineering. Op de manier
waarop het meestal gebruikt wordt, bedoelt men cognitieve complexiteit: de
moeite waarmee mensen software kunnen begrijpen en bedenken. Is deze te hoog,
dan werkt men langzaam en gaat men veel fouten en vergissingen maken. Bij
softwareontwikkeling kunnen kleine fouten echter grote gevolgen hebben: dit is
het beruchte fenomeen 'bugs'. Fouten vertragen de ontwikkeling dus nog verder.
Het is overigens belangrijk om te beseffen dat mensen in het algemeen altijd
fouten blijven maken, ongeacht de hoeveelheid ervaring op het relevante gebied.
Veel ontwikkelmethoden zijn onder andere bedoeld om de complexiteit zoveel mogelijk te verminderen. De achterliggende ideeën zijn vooral gebaseerd op een aantal aspecten van een gangbaar psychologisch model. Volgens dit model kan het menselijk geheugen opgedeeld worden in een korte-termijn- en een lange-termijngeheugen. Alleen het korte-termijngeheugen kan direct willekeurige informatie onthouden die dan makkelijk weer terug te halen is; informatie opnemen in het lange-termijngeheugen kost tijd en moeite. De hoeveelheid informatie die in het korte-termijngeheugen kan worden opgeslagen is echter beperkt: men spreekt van een capaciteit van 5..9 'chunks': chunks zijn concepten die op als één geheel onthouden kunnen worden. Welke concepten als chunks gebruikt kunnen worden hangt af van de mentale representatie die die persoon heeft over de relevante onderwerpen. Het is bijv. aangetoond dat, net als bij andere ontwerptaken, ervaren softwareontwerpers een scala aan relevante chunks tot hun beschikking hebben die beginners niet hebben [Soloway&Ehrlich 89].
Waarschijnlijk vanwege deze geheugenbeperking, blijkt het dat mensen tijdens het ontwerpen nauw samenwerken met externe geheugensteuntjes zoals een vel papier of een computerscherm (men noemt dit 'display-based reasoning'). Mensen blijken dit meer te doen naarmate zij meer ervaring hebben [Davies 93].
Hierop gebaseerd, en in veel verhandelingen over softwareontwikkeling ook vaak
terug te vinden, is de volgende algemene richtlijn: een programma kan het
beste zó opgedeeld moet worden in eenheden, dat deze eenheden als
chunks opgevat kunnen worden. Er moet zo weinig mogelijk interactie zijn
tussen de eenheden, opdat zij zoveel mogelijk los begrepen en beschouwd kunnen
worden. De representatie van een programma moet dan zó zijn opgezet,
dat deze eenheden zo makkelijk mogelijk terug te vinden zijn. Het is echter de
vraag hoe deze opdeling er het beste uit kan zien, en hoe de specificatietaal
of programmeertaal hier het beste aan kan voldoen.
2 De ontwikkelcyclus
Wij zullen eerst de meest gebruikte algemene omtwikkelmethode behandelen, die
vooral tot doel heeft om zoveel mogelijk vooruit te kunnen plannen zodat dure
fouten vermeden kunnen worden [Pennington&Grabowski 90]. Daarna zal bekeken
worden hoe een dergelijke methode kan worden ingevuld met behulp van concrete
beschrijvingen in een bepaalde taal of talen, die vooral tot doel hebben om de
(steeds voorlopige) opzet van de software steeds zo begrijpelijk mogelijk te
maken.
Deze methode wordt beschreven in het watervalmodel, dat ingedeeld is in afzonderlijke fases, die elkaar strikt opvolgen. Het doel van elke fase is dat aan het einde een concrete specificatie opgeleverd wordt, die voldoende is voor het uitvoeren van de volgende fase.
Dit model blijkt in de praktijk niet goed te werken. Hiervoor zijn een aantal oorzaken aan te geven. De belangrijkste is dat tijdens de verschillende fasen van de ontwikkeling veel niveaus van detail tegelijk beschouwd moeten worden, en ook verschillende soorten representaties, die voor het geheel echter wel allemaal van belang zijn. Verschillende representaties laten verschillende aspecten naar voren komen, maar dit gaat vaak ten koste van andere aspecten [Green 90], zodat het makkelijk kan voorkomen dat men een foutief of onvolledig overzicht van het geheel heeft. Hierdoor kan het voorkomen dat de gevolgen van beslissingen pas tijdens het programmeren, of nog later, duidelijk worden. Dit wordt in het algemeen ook beschouwd als een onvermijdelijk gegeven, onafhankelijk van de manier waarop men de methode invult. Sommigen beweren zelfs dat het watervalmodel uitgevonden is door managers om hun managerstaken makkelijker te kunnen uitvoeren, in plaats van dat het wat te maken heeft met hoe software in de praktijk ontwikkeld wordt [Riel 96] [Kitchenham&Carn 90]. Hoe programmeurs in de praktijk neigen te werken wordt ook wel 'opportunistisch' genoemd, ofwel dat elk van de fasen, als deze al als gescheiden geïdentificeerd kunnen worden, zich parallel aan elkaar ontwikkelen, afhankelijk van hoe het in de huidige situatie goed uitkomt. Om toch een vorm van discipline te houden, gebruikt men daarom ook wel een iteratieve versie van het watervalmodel [Riel 96], waarbij men kan terugschakelen naar vorige stappen om onvolkomenheden bij te stellen.
Echter, er valt nog steeds iets aan te merken op het iteratieve model. Het laatste onderdeel, onderhoud, blijkt namelijk verreweg de meeste inspanning te vergen [Meyer 88]. De levensduur van een programma is vaak vele malen groter dan de ontwerptijd van de eerste versie, en het blijkt dat veel software tijdens haar levensduur veel aanpassingen moet ondergaan om onder steeds nieuwe omstandigheden en aan steeds veranderende eisen te blijven voldoen. Het is dus misschien niet realistisch om onderhoud als een enkele aparte fase weer te geven. Men zou het in plaats daarvan kunnen zien als een aspect wat van belang is tijdens alle fases, terwijl de fases zich tijdens de onderhoud van het project blijven herhalen.
Ten slotte is er nog een nieuw aspect wat de laatste tijd meer aandacht
krijgt: hergebruik ('reuse'). Hergebruik houdt in, dat een
al bestaand deelsysteem gebruikt kan worden voor een doeleinde waar het in de
eerste plaats niet specifiek voor geschreven is, het liefst zonder verdere
aanpassingen aan dit deelsysteem. Sommige programma's worden zelfs alleen voor
hergebruik geschreven, bijv. bestandsbeheersystemen, grafische interfaces of
databasepakketten. Dit idee heeft in feite zowat altijd al bestaan, maar wordt
nu steeds interessanter geacht: er wordt een grote hoeveelheid software
geproduceerd waarvan een deel van de onderlinge functionaliteit in feite
steeds hetzelfde is [Lim 94]. Echter, ontwerpen voor hergebruik blijkt een
onzekere aangelegenheid: men kan vaak van te voren niet zeggen of de
mogelijkheden tot hergebruik inderdaad opwegen tegen de extra moeite die nodig
is voor het ontwerpen van herbruikbare software en het rekening houden met de
mogelijkheden tot hergebruik bij het ontwikkelen van nieuwe programma's. Men
zou ook op dit gebied meer zekerheid willen hebben.
2.1 De fasen van de ontwikkelcyclus
De opdeling in de verschillende fasen die wij in het watervalmodel gezien
hebben, is wijd verspreid, en wordt vaak ook gebruikt om observaties t.a.v.
het ontwikkelproces te klassificeren. Bij elk van de fasen zijn er een aantal
specifieke zaken aan te wijzen die van groot belang zijn voor het succesvol
uitvoeren ervan; zie bijvoorbeeld [Pennington&Grabowski 90] en [Green 90].
2.1.1 analyse
Softwareontwikkeling bestaat vaak uit het vertalen van kennis uit een ander
kennisgebied naar software. De programmeur moet dus een beeld krijgen van een
kennisgebied buiten zijn/haar eigen gebied, en dit vertalen naar een
programma, dat meestal een structuur moet hebben die niet eenvoudig
één-op-één af te beelden is op de structuur van
het kennisgebied. Als mensen bekend zijn in een specifiek domein, betekent dat
dus niet dat ze hun kennis en wensen ook in termen van een
computerimplementatie kunnen overdragen. Daarom is het vaak moeilijk om er
achter te komen of een ontwerp voldoet aan de eisen, of zelfs wat de eisen
precies zijn, en kan dit pas geverifieerd worden als er een werkend programma
is. Hier ziet men al de eerste reden waarom het watervalmodel in de praktijk
problematisch is.
2.1.2 ontwerp
Als de eisen min of meer bekend zijn, kan de opzet van het programma bepaald
worden. Observaties op het gebied van ontwerpkeuzes tonen aan dat programmeurs
de neiging hebben om te werken vanuit oplossingen die al in hun hoofd zitten,
in plaats van uit te gaan van wat hun taal of ontwerpomgeving op dat moment te
bieden heeft ten opzichte van het probleem [Schank&Linn 93]. Zij gaan daarbij
vaak uit van deeloplossingen die zij al kennen, die dan aangepast en
samengevoegd worden. Vanuit een simplistisch oogpunt kan men het ontwerpen
dus zien als het samenvoegen van een aantal al bekende ontwerpcliché's,
waarvan men heeft aangetoond dat deze als chunks beschouwd kunnen worden
[Pennington&Grabowski 90]. Men schat het aantal chunks dat bekend is bij
ervaren ontwikkelaars op meer dan 50,000.
De interactie tussen de eenheden van het ontwerp en de inhoud van de eenheden zelf ten alle tijde goed te overzien zijn. Als men een specifiek detail wil weten, moet dit zo direct mogelijk afleesbaar zijn. Een voorbeeld van hoe belangrijk dit is kan men vinden in [Sime et al 77]. Zij onderzochten hoe een representatie van een eenvoudig programma met keuzes (IF-statements) er het beste uit kan zien. Het bleek dat als men voor elke keuze zowel de positieve als de negatieve conditie expliciet weergeeft (wat in feite redundant weergeven van informatie is), men makkelijker fouten terugvindt.
Hier tegenover staat de wenst tot het kunnen wegbergen van irrelevante
informatie (data hiding). Zodra men een beeld heeft gevormd van
een bepaald deel van het ontwerp, moet men de details van dit deel kunnen
weglaten, zodat het overzicht niet 'vervuild' wordt door deze, nu irrelevante,
details. Een voorbeeld hiervan kan men vinden in sommige ontwikkelomgevingen,
waar men lappen code kan 'dichtklappen', die dan door een enkele regel
commentaar vervangen worden.
2.1.3 programmeren
Dit is in feite de voortzetting van het ontwerpen in de hoogste mate van
detail. Een deel van de aantekeningen over ontwerp gelden dus ook voor het
programmeren. Al aangenomen dat de eisen waaraan een programma moet voldoen
bekend zijn, is het vaak niet haalbaar of zelfs niet mogelijk om de correctheid
van een ontwerp te bewijzen. De implementatie is dan de eerste mogelijkheid tot
verificatie van het ontwerp. Hierdoor gaan ontwerp en programmeren vaak nauw
samen.
In deze fase krijgt men voor het eerst direct met de eisen van de
programmeertaal en het computersysteem te maken: dit zijn vooral
efficiëntie en de vorm van de programmeertaal, die in het algemeen zeer
strikt is. Als de programmeertaal niet goed geschikt is om een bepaalde
oplossing weer te geven, dan kan de oplossing in het algemeen wel
geïmplementeerd worden, maar wordt het programma minder goed leesbaar.
2.1.4 testen en debuggen
Het is belangrijk om te realiseren dat er tijdens het software-specificeren
altijd fouten worden gemaakt, ongeacht het ervaringsniveau van de ontwerpers.
Nadat een programma of subprogramma volledig gespecificeerd is, zullen er dus
altijd nog fouten in zitten. Hierdoor is het testen en debuggen een belangrijk
onderdeel van het ontwikkelen.
Testen houdt het bepalen van een testplan in, ofwel het bepalen welke delen van het programma op welke manier getest moeten worden. Het is in het algemeen niet mogelijk om alle mogelijke situaties uitputtend te testen, dus een goed testplan is essentieel. Het testen wordt vergemakkelijkt als een programma uit onderdelen bestaat die relatief weinig onderlinge onafhankelijkheden hebben. Een groot deel van de fouten kan zo gedetecteerd worden door de onderdelen één voor één te testen.
Het is in het algemeen zo dat er fouten in programma's blijven zitten die bij het testen niet gevonden worden, en die zich pas later manifesteren. Dit betekent dat men in principe geen enkel onderdeel van een programma kan vertrouwen als men een fout zoekt. Dit wordt vooral bij grotere systemen een probleem. Ook hierbij is het van nut als de verschillende onderdelen van een programma zo onafhankelijk mogelijk zijn.
Zodra er fout gedrag ontdekt is, moet de locatie van de fout teruggevonden en gerepareerd worden: het debuggen. Van deze twee blijkt het terugvinden de meeste moeite te kosten [Green 90b]. Het debuggen is in feite een heel speciaal geval van het begrijpen van het systeem, omdat het systeem wat men gebouwd heeft (kennelijk) niet aan het beeld voldoet wat men heeft. Het tot in detail kunnen aflezen van de functionaliteit van de al geschreven specificaties is dus essentieel voor het kunnen terugvinden van fouten.
Voor het terugvinden van fouten komt men vaak bepaalde strategieëen tegen, met name de volgende twee [Eisenstadt 97]: het gebruik van debuggers of andere manieren om informatie op te vragen tijdens het draaien van het programma om de manifestatie van de fout duidelijker te zien, en het verregaand beredeneren van de aard van de fout met behulp van specifieke informatie over het foute gedrag van het systeem. Beide worden door verschillende mensen in meer of mindere mate gebruikt.
Alhoewel geavanceerd debugging-gereedschap tegenwoordig vaak beschikbaar is,
blijkt echter dat deze slechts gedeeltelijk van nut is, en weinig verbetering
heeft opgeleverd ten opzichte van weinig of geen debugging-gereedschap
[Weinberg 71] [Eisenstadt 97]. De informatie die men uit debuggers krijgt,
geeft namelijk niet noodzakelijkerwijs beter aan waar men moet kijken om een
fout te vinden. Dit heeft tot gevolg dat debuggers erg veel data geven,
terwijl de hoeveelheid relevante data daarvan beperkt is.
2.1.5 onderhoud
De hoofdtaak van onderhoud is herontwerp van een bestaand systeem, waarbij het
her-analyseren van dit systeem een belangrijke rol speelt. Heel belangrijk is
het kunnen overzien van het effect van aanpassingen (impact
analysis). Het bestaan van onverwacht grote gevolgen van relatief
kleine aanpassingen is een berucht fenomeen: denk bijvoorbeeld aan het
jaar-2000-probleem, waarbij bij een kleine aanpassing aan een veld in
databases (namelijk het toevoegen van een paar extra cijfers in het jaartal)
het hele programma herschreven moet worden om deze aanpassing te reflecteren.
De kunst is om bij het ontwerp al rekening te houden met mogelijk toekomstige
aanpassingen. Echter, zelfs een goed onderhoudbaar ontwerp moet in het
algemeen af en toe herschreven worden, omdat de onderhoudbaarheid slechter
wordt naarmate men willekeurige functionaliteit toevoegt zonder te
herstructureren [Bennett 95].
2.1.6 hergebruik
Het kunnen hergebruiken van functionaliteit is heel aantrekkelijk, maar zoals
al eerder genoemd (zie 2.2.2), hebben mensen in de eerste plaats de neiging om
weinig gebruik te maken van bestaande faciliteiten zolang zij die niet goed
kennen. Onderzoek laat zien dat dit komt doordat het mensen die beperkte tijd
en documentatie krijgen veel moeite bleek te kosten om functies die nieuw voor
hen waren te begrijpen en correct toe te passen [Scholtz 93].
Wat dus met name belangrijk is bij hergebruik, is goede documentatie van het te hergebruiken systeem. Een ander groot probleem is, dat een te hergebruiken systeem zelf vaak ook aan ontwikkeling en veranderingen onderhevig is. De verschillende subsystemen krijgen op deze manier elk hun eigen versie. Het consistent houden van de aansluiting van deze verschillende versies, genaamd versiebeheer (version control), wordt moeilijker naarmate er meer aparte subsystemen zijn [Bennett 95].
Een probleem van hergebruik is, dat er, naast de strikte vorm van de
programmeertaal, er nu ook rekening gehouden moet worden met de structuur van
de te hergebruiken onderdelen. Deze sluiten in de praktijk bijna nooit precies
aan bij de eisen [Garlan et al 95]. Het is vaak echter niet mogelijk om deze
onderdelen zomaar aan te passen, hetzij omdat deze door anderen geschreven
zijn, hetzij omdat zij op dat moment ook door andere programma's gebruikt
worden.
3 Inleiding tot object-oriëntatie
Om te zien wat OO is, en waarom men OO zo goed vindt, zullen wij eerst andere
ontwikkelmethoden bespreken waar OO vaak mee gecontrasteerd en vergeleken
wordt.
3.1 Andere ontwikkelmethoden
Het al eerder genoemde idee van ontwikkelmethoden die het ontwikkelproces
structureren in eenheden met minimale onderlinge afhankelijkheden is al heel
oud. Afhankelijkheden kan men opdelen in control flow en data flow. Een
voorbeeld van grote afhankelijkheden in control flow is het bestaan van
willekeurige sprongen binnen een programma (de 'GOTO', die nu in het algemeen
als ongewenst wordt beschouwd). Een voorbeeld van grote afhankelijkheden in de
data flow is een grote hoeveelheid levende ('live') variabelen, ofwel een
grote hoeveelheid variabelen die allen van belang zijn binnen een bepaald
subprogramma. Het beruchtste zijn globale variabelen, ofwel variabelen die
vanuit elk deel van een programma aanspreekbaar zijn.
3.1.1 De top-down-methode
Eén van de eerste was het gestructureerd ontwerpen, ook
wel de topdown-methode genoemd. Dit houdt in, dat men een
programma beschrijft als een sequentie van een aantal complexe, maar
coherente, bewerkingen. Bij elke bewerking moet expliciet aangegeven worden
welke gegevens hij gebruikt en wat hij daar precies mee doet. Door deze
bewerkingen op hun beurt op te delen in deelbewerkingen, gaat men richting
bewerkingen die zó eenvoudig zijn dat zij direct implementeerbaar zijn.
Deze methode resulteert dus in een hiërarchie van operaties, geordend
naar de volgorde waarin zij uitgevoerd zijn, die men, in overeenstemming met
de eisen van een ergonomische ontwerpstructuur, elk kan zien als een enkele
eenheid, of in detail kan bekijken.
Naast de bewerkingen wordt meestal ook een aantal datastructuren gedefinieerd: dit zijn vaak groepen of reeksen van elementaire datavelden. De belangrijkste datastructuren zijn vaak globaal beschikbaar binnen een reeks bewerkingen of deelbewerkingen, omdat het juist deze zijn die bewerkt moeten worden.
Men heeft een aantal bezwaren aangedragen tegen dit idee, met name de volgende:
De bewerkingen vormen het uitgangspunt, niet de vorm van de data waarop zij werken. Dit betekent dat de methode geen manier aangeeft om de data te structureren: het wordt gestructureerd op zó'n manier, dat de procedures het makkelijkst te implementeren zijn. Het vrij aanspreekbaar zijn van de data zorgt er verder voordat de dataflowafhankelijkheden tussen de delen van het programma groot worden: het resultaat van elke bewerking wordt immers als invoer gebruikt voor de volgende bewerking en is dus van invloed op deze bewerking. Dit heeft tot gevolg dat, als men eenmaal de datastructuren bepaald heeft, men er aan vast zit, omdat de vorm waarin de data geleverd wordt door het hele programma heen gebruikt wordt. Zowel onderhoud als mogelijkheden tot hergebruik worden hierdoor vermoeilijkt.
Een ander bezwaar wat vaak gegeven wordt is dat deze methode geen richtlijn
aangeeft voor het oplossen van een probleem uit een ander kennisdomein: er is
vaak geen direct verband tussen de probleemspecificatie en de reeks operaties
waar men uiteindelijk op uit komt. Anders gezegd, de stap van analyse naar
ontwerp is niet 'natuurlijk'.
3.1.2 De modulaire methode
Een andere methode waarmee OO vergeleken wordt is modulair
ontwerpen: bij deze methode probeert men het ontwerp op te delen in een
aantal subsystemen die elk een specifiek aspect van het programma op zich
nemen. Deze aspecten moeten het liefst zó gekozen worden dat zij
orthogonaal zijn, ofwel dat zij geen overlappende functionaliteit hebben.
Dit heet separation of concerns.
Elke module is in principe vrij in te delen: het kan bestaan uit een aantal losse procedures, definities van datastructuren, en variabelen, die binnen de module vrij aanspreekbaar zijn. Men mag een willekeurig deel hiervan beschikbaar stellen voor gebruik door andere modules: dit deel heet de interface. Van de procedures wordt dan in het algemeen alleen de manier van aanroepen gegeven, samen met een beschrijving van de functionaliteit die zij vervullen, die precies zou moeten beschrijven wat gebruikers van de procedures nodig zullen hebben. De rest is niet aanspreekbaar van buitenaf. Op deze manier is het mogelijk om een duidelijke grens aan te geven tussen waar men wel of niet van uit mag gaan bij het gebruik van de module. Men kan dan een deel van de module vrij veranderen, zolang het gedrag van de interface maar niet verandert. Verder geeft elke module expliciet aan van welke anderen hij functionaliteit gebruikt.
Net zoals men kan zeggen dat een topdownstructuur gericht is op de control flow van een programma, zo is de modulaire methode gericht op de data flow: de modules zijn natuurlijke eenheden voor opdeling in een dataflowdiagram. De control flow is dus ook minder expliciet: binnen een module is deze over het algemeen wel duidelijk, maar wanneer een module een andere aanroept is het niet expliciet aangegeven wat deze module intern precies doet, en in welke volgorde.
Er zijn een aantal voordelen te noemen ten opzichte van top-down: het ontwerp
is onderhoudbaarder omdat, bij het doorvoeren van een verandering, vaker maar
één module veranderd moet worden, vanwege de orthogonaliteit van
modules. Het is ook herbruikbaarder, omdat de orthogonaliteit het makkelijker
maakt sommige modules te hergebruiken in plaats van de benodigde
functionaliteit dubbelop te implementeren. Echter, zoals bij topdown wordt het
probleem van het vertalen van analyse naar ontwerp niet opgelost.
3.2 Wat is OO?
OO is een methode die zijn invloed uitoefent op alle fasen van het
ontwikkelproces: zo heeft men OOA (OO-analyse), OOD (OO-design), en OOP
(OO-programmeren) [Booch 94]. In alle fasen is het hoofdbestanddeel waarmee
men werkt het object. OO is een variant van het imperatieve
paradigma, en is een vervolg op de modulaire opzet.
De herkomst van OO kan men vinden in de programmeertaal Simula 67 [Meyer 88].
Simula is een volledig object-georienteerde taal die origineel ontworpen is om
fysieke systemen te simuleren. Dit idee bleek echter ook heel handig te
gebruiken te zijn voor andere programma's. Sindsdien zijn er veel andere OO
talen bijgekomen: Smalltalk, Object C, Eiffel, C++ en Java, om er maar een
paar te noemen. Er zijn verschillende ideeën over welke kenmerken
essentieel zijn voor OO-talen, maar de volgende kenmerken komt men bijna
altijd tegen [Booch 94][Meyer 88][Pree 95]:
3.2.1 Abstracte datatypen
In OO programmeert men door datatypen te definiëren. Zo'n datatype,
genaamd klasse (class) bestaat uit een hoeveelheid definities van
interne variabelen, een initialisatie- en vernietigingsfunctie, en een reeks
bewerkingen. De klasse is de hoofdeenheid van structurering van een programma,
vergelijkbaar met een module. De interne variabelen zijn van buitenaf niet
direct aanspreekbaar. De beschrijvingen van de bewerkingen en de
initialisatiefunctie vormen hier samen de interface van het object. Deze
beschrijvingen behoren dus eenduidig het gedrag van de klasse te
definiëren. Men noemt een dergelijke opzet ook wel een abstract
datatype (ADT).
Als men een klasse heeft gedefinieerd, kan men een willekeurig aantal variabelen van deze klasse creëren door de creatiefunctie van de klasse aan te roepen. Deze claimt geheugenplaatsen om de interne variabelen in kwijt te kunnen, en initialiseert deze met behulp van de initialisatiefunctie. Het zo ontstane data-object (normaal genoemd object) kan men dan manipuleren door de erop gedefinieerde bewerkingen aan te roepen. De objecten zijn dus dynamisch creëerbaar en ook vernietigbaar. De meeste OO-talen bewerkstelligen dit door middel van referenties naar objecten. In de eerste plaats wijst een referentie nergens naar, totdat een creatiefunctie wordt aangeroepen, en de referentie naar het zojuist gecreëerde object gaat wijzen. De functionaliteit van het object zijn dan aanspreekbaar via de referentie. Bij de betere OO-systemen wordt een object automatisch vernietigd zodra het niet langer gebruikt wordt (garbage collection).
Beschouw als (vaak gebruikt) voorbeeld de stapel [Meyer 88]:
klasse stapel :
-- functienaam parameters resultaat --
----------------------------------------
new () :stack
push (object)
pop () :object
top () :object
isempty () :boolean
end
De functie new() is de initialisatiefunctie. Deze zorgt ervoor
dat de interne variabelen zó worden geïnitialiseerd, dat deze
overeen komen met een lege stapel. De andere functies doen wat men zou
verwachten bij een stapel: een object erop leggen, push(object),
een object er weer afhalen, pop(), welke het object oplevert wat
eraf gehaald is, het bovenste element bekijken, top(), en het
testen of de stapel leeg is, isempty(). Deze definitie van de
stapel vindt men in ongeveer dezelfde vorm terug in de standaardbibliotheken
van verscheidene OO-talen.
Hoe de stapel in het geheugen bijgehouden wordt, of hoe de functies intern opereren, is niet relevant, zolang ze maar doen wat men ervan verwacht. Goede bescrijvingen van de operaties zijn dus heel belangrijk. Volgens de formele definitie van ADT moet voor een klasse zelfs een aantal wiskundige stellingen (invarianten ofwel axioma's) worden gegeven, die voldoende moet zijn om de correctheid van willekeurig gebruik van de klasse te kunnen bewijzen. In de programmeertaal Eiffel kan men bijvoorbeeld invarianten aangeven door precondities en postcondities [Meyer 88]. Daarom wordt OO programmeren ook wel declaratief genoemd, in tegenstelling tot procedureel, waarbij men functies uitlegt door algoritmen te beschrijven [Green 90] [Détienne 90].
Echter, wel moet worden opgemerkt dat sommige dingen moeilijk zijn te
beschrijven door middel van wiskundige stellingen, en dat de invarianten die
men expliciet in de taal kan aangeven (zoals bijv. bij Eiffel) dan ook lang
niet alles kunnen controleren. In de praktijk valt het zelfs op dat bij de
beschrijving van klassen, bijv. die van de Borland C++ standaardklassen,
bijna nooit dergelijke stellingen gegeven worden, zelfs niet in een
natuurlijke-taalvorm. Wat men juist vaak ziet is dat het gedrag van een klasse
op een procedurele manier beschreven wordt. Dit geeft wel te denken over de
correctheid van het wijd verbreid gebruik van de term ADT. Men zou immers zo
elk datatype een ADT kunnen noemen. In dit geval zouden abstracte datatypes
eigenlijk beter vrij definieerbare datatypes kunnen heten.
3.2.2 Polymorfisme en overerving
Talen die volgens het eerste principe werken worden ook wel
object-gebaseerd genoemd. Als zij ook de volgende principes
ondersteunen, dan heten ze object-georienteerd.
Het eerste principe is overerving (inheritance). Dit principe gaat min of meer gelijk op met de KI-theorie van Semantic Nets [Shastri 88]. Deze gaat ervan uit dat menselijke kennis te modelleren is door middel van concepten (vergelijk met objecten), die in een hiërarchie van klassen ingedeeld zijn. Het denken over de concepten gaat aan de hand van stellingen die gelden voor elke klasse en de klassen die eronder zitten (dus ervan afgeleid zijn) in de hiërarchie. Deze laatste eigenschap wordt daarom ook wel overerving van eigenschappen genoemd. In OO-talen is het mogelijk om interne variabelen en bewerkingen van andere klassen over te erven, om er vervolgens eigen variabelen en bewerkingen aan toe te voegen.
Daarnaast is er polymorfisme. Polymorfisme is de mogelijkheid, dat men een
bepaalde operatie kan definiëren die op verschillende klassen uitgevoerd
kan worden met tot op zekere hoogte hetzelfde effect, maar in detail aangepast
wordt aan de aard van de klasse. Een polymorfe operatie definieert dus in
feite niet een enkele operatie, maar een groep operaties. Polymorfisme gaat
meestal samen met overerving: een voorouder definieert een operatie, en
specificeert het algemene deel ervan. De afstammelingen kunnen deze operatie
dan elk naar eigen wens invullen met hun eigen variant, zo lang ze zich aan
het algemene deel houden.
3.3 Waarom OO?
De voorstanders van OO beweren dat het een aantal voordelen heeft boven andere
methoden. Het biedt, naast de al genoemde voordelen van het modulaire ontwerp,
nog eens de volgende voordelen:
Betere herbruikbaarheid. Het is mogelijk om eigen klassen te definiëren door het overerven van bestaande klassen, waarna men naar behoeven aanpassingen maakt. Dit principe wordt ook wel programming by difference genoemd. Daarnaast biedt polymorfisme de mogelijkheid om de functionaliteit van operaties binnen een klasse te parametriseren, zodat deze nog flexibeler worden, en daardoor nog meer mogelijkheden tot hergebruik leveren.
Intuïtievere afbeelding van een ander kennisdomein naar een ontwerp. De
klassificering in een hiërarchie van klassen geeft een mogelijkheid tot
een 'natuurlijke' opsplitsing van het probleem, omdat het zo mogelijk is om
concepten uit de werkelijkheid één-op-één te
vertalen naar klassen in een OO-ontwerp. Mits de theorie van de semantische
netwerken een goed model is van hoe mensen werkelijk denken, zou men kunnen
beargumenteren dat programma's met een OO-indeling inderdaad makkelijker door
mensen op te stellen en te begrijpen zijn.
4 OO in de praktijk: waar liggen de problemen?
Gelden deze beweringen ook voor programma's van serieuze grootte en
complexiteit? Om een idee te krijgen van de typische omvang van een OO
programma: de Borland C++ en Eiffel standaardbibliotheek bestaan elk uit zo'n
50 verschillende klassen. InterViews, een GUI-pakket, bestaat uit zo'n 150
klasses, en meer dan 500 operaties in totaal [Kung et al 94]. Andere dergelijke
pakketten bevatten meestal tussen de 250 en 1000 operaties.
De algemene ervaringen zijn dat OO niet zo ideaal is als gedacht; er zijn geen gegevens die eenduidig onderbouwen dat OO echt beter werkt [Daly et al 95]. Kennelijk zijn er, naast de voordelen, ook problemen. Onder andere rapporteert men veel problemen bij het overschakelen naar OO, wat tegen de claim van natuurlijkheid ingaat [Détienne 90], en moeilijkheden bij het teruglezen van OO code, zelfs door ervarenen. Sommigen zijn zelfs heel pessimistisch: zij zeggen dat onderhoud een ware kunst wordt vanwege onverwacht grote afhankelijkheden [Li&Jefferson Offutt 96] [Kung et al 94]. Men kan de volgende hoofdproblemen aanwijzen:
Daarnaast kan men zeggen dat fysieke objecten en hun interacties subtiel en ingewikkeld zijn, zodat een modellering noodzakelijkerwijs onvolkomen is en slechts een bepaald aspect van de werkelijkheid kan weergeven [Green 90b]. Zelfs als men alleen een bepaalde toepassing ontwerpt, zijn de opdelingen waarop men uitkomt soms heel verschillend, elk met hun eigen voor- en nadelen [Shaw 95]. Opvallend was dat voor eenzelfde probleem vaak dezelfde objecten worden geïdentificeerd, maar dat de relaties ertussen nogal verschillend gekozen kunnen zijn. Daarnaast bleek het dat mensen, vanwege de grote hoeveelheid keuzemogelijkheden waarvan het van te voren nog niet duidelijk is welke beter zijn, bijv. moeite hebben om te kiezen tussen iets modelleren als klasse of als attribuut [Détienne 90]. Het idee dat het ontwerp vergemakkelijkt wordt door het direct kunnen modelleren van objecten en hun relaties gaat dus niet zonder meer op.
Dit gaat ook op voor de structurering die men kan aangeven door middel van
overerving. Overerving gezien als model van menselijke kennis is een te
simplistisch beeld: de manier waarop men klassificeert is afhankelijk van het
gezichtspunt van waaruit men het op dat moment doet. Elke klassificatie heeft
weer zijn eigen voor- en nadelen. Ook hier is de beste opdeling dus sterk
afhankelijk van de eisen van de toepassing [Riel 96] [Pree 95]. Vaak worden er,
om de beperkingen van de strikt hiërarchische opdeling te omzeilen, meer
mogelijkheden aan de taal toegevoegd, zoals bijv. meervoudige overerving.
Echter, hierdoor wordt de al grote hoeveelheid keuzemogelijkheden nog groter.
4.1.2 Bezwaren tegen ADT als enige structureringsmethode
In OO is het niet alleen mogelijk om eigen datatypes te
definiëren; het is de enige mogelijkheid om een programma te
structureren. Het blijkt echter, dat er programmastructuren zijn, die
noodzakelijk zijn, maar niet goed in een ADT-structuur passen [Szyperski 92].
Van sommige concepten is er altijd precies één; men heeft bijv.
maar één toetsenbord en één muiscursor. Het
levert vaak ongewenste (en voor de meeste gevallen onnodige) complexiteit op om
het systeem zó te maken dat er meerdere van deze kunnen zijn. Echter,
in OO is het niet zonder meer mogelijk om af te dwingen dat er ooit maar
één object van een bepaalde klasse gecreëerd mag worden
zonder een aparte globale te definiëren die dit bijhoudt. Bij de andere
paradigma's was dit helemaal geen probleem; men kan gewoon bijv. een losse
functie readkeyboard definiëren. Echter, in OO moet deze
functie altijd deel uitmaken van een object, bijv. object
keyboard, waarvan er ook meteen meerdere gecreëerd kunnen
worden.
Sommige operaties zijn niet natuurlijk bij een enkel object te plaatsen, omdat
zij werken op meerdere objecten, bijv. bewerkingen die gegevens converteren,
samenvoegen, of extraheren, of operaties die meerdere verschillende typen als
parameters hebben. Vaak wordt dan een vrij willekeurige keuze gemaakt, welke
de structuur van het programma niet altijd ten goede komt. Een ander
gerelateerd bezwaar is, dat onder sommige omstandigheden elk object dreigt te
worden vergeven van de functies. Dit gebeurt op het moment dat er uitgebreide
bibliotheken zijn voor dat bepaalde datatype, bijv. een aantal uitbreidingen op
de standaard numerieke functies zoals matrices en integraalbenaderingen. Men
zou deze allemaal bij de klasse float moeten zetten, terwijl de
meeste programma's de meeste uitbreidingen nooit zullen gebruiken. Afgezien
van de efficiëntieproblemen die zo optreden, dreigt men op deze manier het
overzicht kwijt te raken.
Een ander probleem is de ontoegankelijkheid van de interne structuur van elk
datatype. Psychologisch gezien heeft men een concreet beeld van een bepaald
datatype, welke dan niet altijd overeenkomt met het werkelijke datatype,
waardoor men in complexe situaties verkeerde keuzes zou kunnen maken. Beschouw
als elementair voorbeeld weer de stapel. Bij de abstracte stapel is het niet
mogelijk om de stapelwijzer op te vragen, alhoewel de definitie wel de
aanwezigheid ervan suggereert. Echter, zonder stapelwijzer is een belangrijk
deel van de functionaliteit van een stapel niet mogelijk, met name het
definiëren en opruimen van stackframes, terwijl deze toch zé
veelvuldig gebruikt worden, dat veel microprocessors hier speciale instructies
voor hebben. Echter, had men wél een stapelwijzer gedefinieerd, als
een aparte ADT, dan had men twee ADT's gehad die echter wel op een bepaalde
manier samenhangen. Dergelijke constructies heten
ontwerpsjablonen (design patterns). Echter, deze brengen weer
andere problemen met zich mee, zoals wij zullen zien in 4.3.2.
4.1.3 Hergebruik in OO resulteert vaak in teveel extra complexiteit
OO biedt een aantal faciliteiten waarvan de voorstanders beweren dat deze
hergebruik gemakkelijker zullen maken en tot hergebruik uitnodigen. Men is in
staat om klasses slechts gedeeltelijk te definiëren om deze later naar
believen verder in te vullen of er delen aan toe te voegen.
Echter, het blijkt vaak moeilijk om programma's zö te schrijven, dat zij herbruikbaar zijn. Een aantal oorzaken hiervan kan men terugvinden in [Garlan et al 95], die hebben geprobeerd om een aantal bestaande (voor een deel object-georienteerde) voor hergebruik gemaakte programma's samen te voegen tot een gebruikersomgeving. Hierbij ondervonden zij meer tegenslag, en moesten meer aanpassen, dan verwacht, en het resulterende systeem was zó onefficiënt, dat het bijna onbruikbaar was. De problemen waar zij tegenaan liepen bestonden voor een groot deel uit verkeerde aannames die slecht uitpakten. Als voorbeeld werden er o.a. de volgende genoemd: Er bleek soms een conflict te zijn over in welk programma nu de hoofdlus van het systeem zat. Een slechte aansluiting van protocollen bleek veel extra complexiteit op te leveren in applicatieprogramma's waarvoor deze protocollen niet eens interessant waren. De onderlinge afhankelijkheden tussen de verschillende systeemcomponenten bleken groter dan gedacht, welke o.a. het versiebeheerprobleem verergerde. Een deel van de problemen waren dus control-flowproblemen, waavoor OO niet zonder meer een oplossing biedt. Daarnaast waren de architecturen van de deelsystemen kennelijk niet zo helder en duidelijk dat de aannames en afhankelijkheden van te voren goed ingeschat konden worden.
Men kan samenvattend het volgende stellen: programma's zijn vaak alleen zonder
meer herbruikbaar tot in zoverre als de ontwerpers al van te voren rekening
hebben gehouden met de mogelijke toepassingen. [Riel 96] geeft hiervan een
voorbeeld: hij had een 'algemene' klasse linked list, waar hij
een afgeleide klasse circular linked list van wilde maken.
Echter, dit bleek niet te kunnen, omdat de algemene klasse ervan uitging dat
een linked list altijd een begin en een einde heeft. Dit had, door de
ontwerper van de linked list, opgelost kunnen worden door bepaalde van de
functies polymorf te maken. Achteraf moet men eigenlijk de
overervingsstructuur veranderen op dit netjes op te lossen.
Dergelijke maatregelen vooraf (Riel noemt deze 'reusability hooks') resulteren
echter in een hogere complexiteit. Neem als een wat groter voorbeeld een
bibliotheek van tekenfuncties. Normaal gesproken werken deze in twee
dimensies. Echter, goede reusability hooks zouden bijv. ook de optie toestaan
om in drie dimensies te kunnen tekenen. Echter, hiermee haalt men zich wel
een hoop extra moeilijkheden op de hals: werkt men met een verdwijnpunt of
isometrisch? Aangezien de projectie niet langer bijectief is, hoe definieert
men dan het lezen van een pixel, zoals deze bestaat voor twee dimensies? Zijn
bepaalde functies wel toepasselijk op drie dimensies? Zou men niet ook meteen
vier of meer dimensies toestaan? De 'gewone' ontwerper, die in twee dimensies
wil tekenen en niet geïnteresseerd is in dergelijke vraagstukken, wordt
dan opgescheept met het beantwoorden ervan.
4.2 Versnippering van functionaliteit
4.2.1 Het control-flow-aspect van het programmeren wordt op de achtergrond
gezet.
[Kung et al 94] laten zien dat het soms moeilijk is om OO-code te begrijpen;
zij observeerden dat een ervaren C++-programmeur soms twee uur nodig had om
een enkele functie van twintig regels uit een bepaalde klasse van het
GUI-pakket InterViews te begrijpen. Dit kan men toeschrijven aan de volgende
zaken: Ten eerste, om een functie uit een klasse te begrijpen, moet men het
gecombineerde effect van het geheel aan functies van de klasse eerst
begrijpen, omdat deze nauw samenhangen. Dit is echter soms moeilijk, omdat de
functies in willekeurige volgordes door andere klassen kunnen worden
aangeroepen, en de interne variabelen van de klasse in feite globalen zijn
voor de functies binnen de klasse. Ten tweede, het wordt extra moeilijk in het
geval delen van het systeem nog niet goed bekend zijn. Vaak roepen functies
namelijk andere, dan ook niet precies bekende, functies aan. Deze
moeilijkheden zijn bij bijv. een modulair ontwerp ook het geval, maar het
verschil is dat er meestal veel minder modules zullen zijn dan klassen, en
gebruikte variabelen binnen een module niet zo gauw globaal voor deze module
zullen worden gedefinieerd, maar eerder expliciet als parameter zullen worden
meegegeven voor elke functie die hen nodig heeft.
Daarnaast heeft overerving tot resultaat dat alle functies die er bij een klasse horen niet allemaal bij elkaar staan, zodat men het geheel aan functionaliteit van een klasse voor een deel moet opzoeken in de code van andere klasses. [Daly et al 95] onderzochten klachten die programmeurs hadden over het begrijpen van programma's met diepe overervingshiërarchieën. Een toevoeging aan een 3 niveaus diepe overervingstructuur voor een eenvoudig probleem bleek slechts 20% sneller implementeerbaar te zijn dan dit zelfde probleem zonder overerving, waarbij alle functionaliteit opnieuw geschreven moest worden. Een voorstudie liet zien dat bij diepere hiërarchieën, de toevoegingen met overerving juist moeilijker worden dan zonder overerving.
[Détienne 90] laat zien dat programmeurs die geen ervaring hebben met
OO problemen hebben met de opzet van hun programma's, welke tegen de claim van
het natuurlijk zijn van OO ingaat. Eén van de problemen die hij
identificeerde was, dat OO forceert dat het declaratieve aspect eerst gedaan
moet worden voordat men aan het procedurele aspect kan beginnen.
Ander onderzoek naar het verschil in programmeerstijl tussen ervaren en
niet-ervaren OO programmeurs laat zien, dat het grootste verschil niet zozeer
ligt in de keuze van opdeling in klasses ligt, maar in het begrijpen van de
control flow [Agarwal et al 96].
[Davies et al 95] onderzochten de manier waarop OO programmeurs met
programma's omgaan door de manier waarop zij klassificeren te bekijken: er
werd gevraagd om een aantal losse stukjes code op te delen in
categorieën. Het bleek dat de klassificaties van de meer ervaren
programmeurs (lopend van 1 jaar ervaring tot 3 jaar ervaring en bekendheid met
het programma) van klasse-gebaseerd richting functie-gebaseerd ging. Opvallend
was, dat het leek alsof de opdeling steeds oppervlakkiger werd, in plaats van
inhoudelijker, zoals geobserveerd was bij onderzoek naar andere
programmeertalen [Schank&Linn 93]. Kennelijk is een probleemfactor van het OO
programmeren gelegen in het volgen van de control flow.
4.2.2 Moeilijk te debuggen
Data hiding werkt, zolang men erop kan vertrouwen dat de niet bekeken delen van
het programma inderdaad werken zoals verwacht. Echter, als er iets misgaat,
dan bevat in sommige gevallen de verborgen data ineens relevante informatie.
Neem als voorbeeld een ingeburgerde ADT: het schijfbestand. Zolang alles goed
gaat, hoeft men niet te weten hoe bestanden opgeslagen worden, maar als de
schijf fysieke of logische defecten vertoont (bijv. vanwege bugs in het
bestandsbeheer), dan ben je opeens overgeleverd aan het bijgeleverde
reparatiegereedschap, als men er al aan gedacht heeft om deze mee te leveren.
Men kan er niet op rekenen dat er geen fouten zitten in bestaande delen van
een programma, zelfs na uitgebreid testen. In het geval er een fout optreedt,
moet men dus eigenlijk het gehele programma, instructie voor instructie,
kunnen nakijken. In het geval van OO wordt dit vaak vermoeilijkt omdat er in
een willekeurige functie zoveel andere worden aangeroepen. Sommige aanroepen
zijn ook impliciet, zoals creatie- en vernietigingsfuncties, en geneste
aanroepen. Men kan beargumenteren dat dit soort fouten weinig voorkomt, maar
het zijn nu juist wèl de fouten die alleen af en toe optreden, of
optreden op een plaats waar men ze niet verwacht, die verreweg het moeilijkst
te vinden zijn, en soms zelfs nooit teruggevonden worden [Eisenstadt 97].
4.3 Onverwacht grote afhankelijkheden
4.3.1 Overerving en polymorfisme leveren veel onderlinge afhankelijkheden
op.
Polymorfisme is een krachtig gereedschap, maar kan makkelijk tot misbruik
leiden. het is extra moeilijk om de betekenis te begrijpen van wat in feite
een vaag gedefinieerde operatie is. Een voorbeeld van hoe verwarrend het kan
zijn is het gebruik van de +-operator voor strings, zoals dit wel
gedaan wordt in C++. De + wordt normaal gebruikt voor optelling,
maar wanneer het toegepast wordt op strings, betekent het concateneren. Dit
mag voor velen intuïtief duidelijk zijn, maar mensen met een wiskundige
achtergrond kunnen hier wel eens van door de war raken, aangezien zij gewend
zijn dat de + altijd commutatief is. We zijn in 3.2.1 al de
beperkingen van het formeel kunnen beschrijven van functies tegengekomen. Voor
polymorfisme is dit een nog groter probleem, omdat deze in feite een
functieruimte definiëren.
De ontwerpers van Java hebben dergelijke herdefinitie van operatoren (operator-overloading) al met opzet weggelaten omdat het volgens hen slecht onderhoudbare code oplevert. Vanuit het onderhoudsstandpunt gezien veroorzaakt het gebruik ervan namelijk een grotere afhankelijkheid in het systeem: bij elke verandering van of toevoeging aan een klasse met een polymorfe functie kunnen niet alleen zijn afstammelingen van semantiek veranderen, maar ook zijn polymorfe voorouders.
Daarnaast levert overerving ook te grote afhankelijkheden op, namelijk die
tussen een klasse en zijn afstammelingen. Een eerder genoemde eis van goede
modulariteit is, dat de modules orthogonaal moeten zijn. Als men overerving
gebruikt is dit niet langer zo: de onderhoudbaarheid neemt af [Garlan et al
95]. Zoals wij hebben gezien in 4.1.3 heeft dit soms tot gevolg dat, om het
ontwerp intuïtief te houden, men een deel van deze structuur om moet
gooien.
4.3.2 Afhankelijkheden bij andere coherente structuren met meerdere
objecten
Naast polymorfisme en overerving is er nog een ander soort afhankelijkheid,
welke vaak aangeduid wordt met de term 'uses'-relatie. Een
coherente groep objecten die verbonden zijn via uses-relaties worden ook wel
een ontwerpsjabloon genoemd [Pree 95]. Echter, bij ontwerpsjablonen moet men
afhankelijkheden kunnen aangeven die voorbij het enkele object gaan. Een
eenvoudig voorbeeld wat we al zijn tegengekomen in 4.1.2 is de stapel met
stapelwijzer. Elke stapelwijzer hoort altijd bij één stapel; men
mag geen stapelwijzers van verschillende stapels door elkaar heen gebruiken.
het blijkt echter dat het moeilijk is om dergelijke constraints tussen
verschillende objecten en klasses aan te geven; deze passen niet natuurlijk in
de OO-structurering, waar het object de hoofdeenheid is. In structureringen
waar men meer vrijheid heeft, zoals de modulaire, is dit minder een probleem
[Szyperski 92] [Linden et al 95].
Daarnaast wordt er vaak weinig aandacht besteed het probleem van samenwerken
met andere, autonome, subsystemen, welke nog meer afhankelijkheden kan
veroorzaken. Beschouw als voorbeeld het openen van een schijfbestand. In
feite is er een afhankelijkheid ontstaan tussen het schijfbestand gegeven door
zijn specifieke bestandsnaam en het object. Als nu een ander object hetzelfde
bestand opent, dan is er een afhankelijkheid onstaan tussen objecten die erg
moeilijk uit de structuur van het programma af te lezen valt.
4.3.3 Dynamische allocatie en referenties naar objecten
Het idee van referenties bestaat in zowat alle imperatieve talen: zij komen
meestal in de vorm van pointers. Teveel gebruik van pointers wordt vaak als
ongewenst beschouwd, vooral omdat deze te direct toegang geeft tot de
representaties van gegevens in het fysieke geheugen. Bij een goed ontworpen
OO-taal hoeft dit niet het geval te zijn; men kan referenties zó
implementeren, dat deze niet als pointer naar harde geheugenplaatsen
gebruikt kunnen worden. Echter, een gedeelte van de moeilijkheid van pointers
geldt ook voor het algemenere geval van referenties: zij kunnen aanleiding
geven tot een geniepig soort afhankelijkheid, genaamd aliasing
[Minsky 96].
Het probleem komt in feite al naar voren als men kijkt naar de problemen die men krijgt zonder de aanwezigheid van automatische garbage collection. Deze blijkt namelijk bijna noodzakelijk te zijn in OO-talen, anders raakt men te gauw geheugen kwijt omdat stukken geheugen die niet meer gebruikt worden per ongeluk niet meer vrijgegeven worden (memory leak). Dit is dan ook een berucht probleem in de taal C++, die geen automatische garbage collection heeft. Deze noodzaak is een gevolg van het feit, dat functies vaak objecten opleveren waarvan het niet duidelijk is of men deze zelf moet opruimen of niet. Als dit automatisch gebeurt, hoeft men zich hier geen zorgen meer over te maken. Echter, dit is eigenlijk slechts een oppervlakkige oplossing voor een dieperliggend probleem: als het al onduidelijk is wie er naar een object wijst, dan is de semantiek van het programma in feite ook onduidelijk. Neem als voorbeeld weer de stapel. Wat gebeurt er als je een object opvraagt met top(), en daarna een pop() uitvoert? Bestaat het object dan nog of is het vernietigd? Wat als je push((top()) doet? Wordt de inhoud van het object gekopieerd of staat er nu een dubbele referentie op de stapel? Wat als je in dat geval weer pop() doet? Opvallend is, dat dergelijke problemen in de meeste uitleggingen over OO niet behandeld worden. In [Meyer 88] bijvoorbeeld, wordt de stapel als ADT behandeld, maar uit zijn beschrijving is niet op te maken hoe de stapel op de bovengenoemde operaties reageert.
In feite is, door het kunnen doorgeven van referenties, de veiligheid van de data binnen een object niet gewaarborgd. Er kan data 'uitlekken' naar andere objecten, die dan per ongeluk deze data veranderen of juist aannemen dat de data constant blijft. Soms wordt dit ook een veiligheidsprobleem in de andere zin van het woord: sommige van de veiligheidsproblemen met Java bijvoorbeeld, zijn een gevolg van dit fenomeen: één van de problemen was, dat men de privilegelijst die men kan opvragen per ongeluk ook kon veranderen, zodat een Java-applet zichzelf een hogere privilege kan geven [Felten 97].
Ten slotte levert de meestal nogal impliciet gehouden aanwezigheid van
referenties wel eens identiteitsmisverstanden op. In de meeste OO-talen wordt
in bepaalde gevallen namelijk wèl een onderscheid gemaakt tussen een
object en een referentie naar een object. Java gebruikt hiervoor twee
verschillende termen: equality (objecten hebben gelijke inhoud) en identity
(referenties zijn gelijk). In Java zijn er een aantal basistypes die nooit met
behulp van een referentie werken, terwijl alle andere types dit wel doen. De
==-operator betekent voor primitieve objecten equality, maar voor
de andere objecten identity. Voor het toetsen van equality van andere objecten
moet men weer een andere operatie gebruiken. De betekenis van ==
is dus anders voor verschillende objecten. Echter, een andere vraag die nu
rijst is, hoe de inhoud van objecten vergeleken moet worden: deze bestaat
immers weer voor een deel uit verwijzingen. In Eiffel bestaan er twee
verschillende operaties hiervoor, genaamd is_equal en
is_deep_equal, welke resp. de inhoud van de velden van een object
niet en wel recursief aflopen. Het is dus niet triviaal welke gelijkheid
er onder welke omstandigheden bedoeld wordt.
5 Conclusies
OO wordt steeds meer als definitieve vervanger van voorgaande paradigma's
gezien, zelfs door degenen die de problemen ervan onderkennen. In deze tekst is
gepoogd om het tegengestelde standpunt in te nemen. Dit leverde een aantal
interessante probleemstellingen op, die misschien juist iets zeggen over de
problemen van softwareontwikkeling in het algemeen. Echter, er is ook
geprobeerd te beargumenteren dat OO zeker niet onder alle omstandigheden beter
is dan zijn voorgangers. Op sommige gebieden biedt het weliswaar gemakkelijk
toegang tot een aantal krachtige mogelijkheden, maar de kunst van het maken van
een goed ontwerp is niet minder belangrijk geworden, maar eerder nog
belangrijker, omdat het nog moeilijker is geworden om dit krachtige gereedschap
juist te hanteren.
6 Referenties
[Agarwal et al 96], Ritu Agarwal & Atish P. Sinha & Mohan Tanniru,
The role of prior experience and task characteristics
in object-oriented modeling: an empirical study,
International journal of human-computer studies 45, blz 639-667
[Anderson & Jeffries 85], John R. Anderson & Robin Jeffries, Novice LISP errors: undetected losses from working memory, HCI Volume 1, blz 107-131
[Bennet 95], Keith Bennet, Legacy systems: coping with success, IEEE Software, january 1995, blz 19-23
[Booch 94], Grady Booch, Object-oriented analysis and design, second edition, Benjamin/Cummings publishing.
[Daly et al 95], J. Daly & A. Brooks & J. Miller & M. Roper & M. Wood, The effect of inheritance on the maintainability of object-oriented software: an empirical study, Proceedings of the 1995 international conference on software maintenance
[Davies 93], Simon P. Davies, Expertise and display-based strategies in computer programming, People and computers VIII: proceedings of the HCI '93 conference
[Davies et al 95], Simon P. Davies & David J. Gilmore & Thomas R.G. Green, Factors influencing the classification of object-oriented code: Supporting program reuse and comprehension, Symbiosis of human and artifact, advances in human factors/ergonomics 20A
[Détienne 90], Francoise Détienne, Difficulties in designing with an object-oriented language: an empirical study, INTERACT '90
[Eisenstadt 97], Marc Eisenstadt, My hairiest bug war stories, Communications of the ACM, vol.40 (april 1997), blz 30-37
[Felten 97], Edward Felten, HotJava 1.0 Signature Bug, http://www.cs.princeton.edu/sip/news/april29.html
[Garlan et al 95], David Garlan & Robert Allan & John Ockerbloom, Architectural mismatch: why reuse is so hard, IEEE Software, November 1995, blz 17-26
[Green 90], T.R.G. Green, Programming languages as information structures, Psychology of programming (computers and people series), blz 118-137
[Green 90b], T.R.G. Green, The nature of programming, Psychology of programming (computers and people series), blz 21-44
[Kitchenham & Carn 90], Barbara Kitchenham & Roland Carn, Research and practice: software design methods and tools, Psychology of programming (computers and people series), blz 271-284
[Kung et al 94], D. Kung & J. Gao & P. Hsia & F. Wen & Y. Toyoshima & C.Chen, Change impact identification in object oriented software maintenance , Proceedings of the 1994 international conference on software maintenance
[Li & Jefferson Offutt 96], L. Li & A. Jefferson Offutt, Algorithm analysis of the impact of changes to object-oriented software , Proceedings of the ICSM '96
[Lim 94], Wayne C. Lim, Effects of reuse on quality, productivity, and economics, IEEE Software, september 1994, blz 23-30
[Linden et al 95], Frank J. van der Linden & ju"rgen K mu"ller, Creating architectures with building blocks, IEEE Software, November 1995, blz 51-60
[Meyer 88], Bertrand Meyer, Object-Oriented Software Construction, Prentice Hall international series in computer science
[Minsky 96], Naftaly H. Minsky, Towards alias-free pointers, ECOOP '96
[Monroe et al 97], Robert T. Monroe & Andrew Kompanek & Ralph Melton & David Garlan, Architectural styles, design patterns, and objects, IEEE Software, january/february 1997
[Pair 90], C. Pair, Programming, programming languages, and programming methods, Psychology of programming (computers and people series), blz 9-19
[Pennington & Grabowski 90], Nancy Pennington & Beatrice Grabowski, The tasks of programming, Psychology of programming (computers and people series), blz 45-62
[Pree 95], Wolfgang Pree, Design patterns for object-oriented software development, Addison-Wesley
[Riel 96], Arthur J. Riel, Object-oriented design heuristics, Addison-Wesley
[Schank & Linn 93], Patricia K. Schank & Marcia C. Linn, Supporting Pascal programming with an on-line template library and case studies, International journal of man-machine studies 38, blz 1031-1048
[Schneiderman 82], B. Schneiderman, Control flow and data structure documentation: two experiments, Communications of the ACM, Vol. 25, Nr. 1, blz 55-63
[Schneidewind et al. 96], Norman Schneidewind & Ned Chapin & Ted Keller & Tom Pigoski & Nicholas Zvegintzov, Panel on: "How much has software maintenance changed since 1983?", Proc. of the 1996 int. conf. on software maintenance
[Scholtz 93], Jean Scholtz, A longitudinal study of transfer between programming language by experienced programmers, People and computers VIII: proceedings of the HCI '93 conference
[Shastri 88], Lokendra Shastri, Semantic Networks: an evidential formalization and its connectionist realization, research notes in artificial intelligence
[Shaw 95], Mary Shaw, Comparing architectural design styles, IEEE Software, November 1995, blz 27
[Sime et al 77], M.E. Sime & A.T. Arblaster & T.R.G. Green, Structuring the programmer's task, Occupational psychology 50, blz 205-216
[Smith et al 95], Simon Smith & Keith Bennett & Cornelia Boldyreff, Is maintenance ready for evolution?, Proc. of the 1995 int. conf. on software maintenance
[Soloway&Ehrlich 89], Elliott Soloway & Kate Ehrlich, Empirical studies of programming knowledge, Software reusability vol. II: applications and experience, blz 235-267
[Spuler 93], David A. Spuler, C++ and C debugging, testing and reliability
[Szyperski 92], Clemens A. Szyperski, Import is not inheritance; why we need both: modules and classes ECOOP '92, blz 19-31
[Weinberg 71], G.M. Weinberg, The Psychology of Computer Programming