Overerving

Ontwerp

Overerving in objectgeoriënteerde softwareontwikkeling is het idee dat een groep klassen hun overeenkomstige onderdelen (attributen en operaties) specificeren op een gedeelde centrale plek, zodat deze onderdelen maar 1 keer geïmplementeerd hoeven te worden, maar wel op meerdere plaatsen gebruikt kunnen worden. Het grote voordeel hiervan is uiteraard het feit dat het maar 1 keer geïmplementeerd hoeft te worden, want dit maakt het ook mogelijk om deze onderdelen eenvoudig te wijzigen voor alle klassen die er gebruik van maken. In tegenstelling tot dezelfde functionaliteit op meerdere plaatsen implementeren en dan dus ook op meerdere plaatsen tegelijk moeten wijzigen. Daarnaast is het mogelijk om vanaf de centrale plek af te dwingen dat alle klassen die gebruik willen maken van de centrale implementatie zelf ook aan een bepaalde structuur voldoen. Zodoende is het mogelijk om garanties voor aanwezigheid van functionaliteit af te geven.

Voorbeeld

Stel je voor dat we een spel gaan maken waarin karakters voorkomen van 3 verschillende typen, te weten krijgers Warrior, tovenaars Sorcerer en schurken Rogue elk met hun eigen talenten en specialismen. Afgezien van wat er allemaal nog meer mogelijk is in het spel, zou ik het stuk van de karakters kunnen ontwerpen zoals onderstaand klassendiagram. Uiteraard is dit klassendiagram niet compleet want de overige onderdelen van het spel worden niet genoemd, en de operaties hebben (bewust) geen parameters, maar het dient nu als voorbeeld.

full fig.1: De genoemde klassen Warrior, Sorcerer en Rogue uitgewerkt in een klassendiagram.

Zoals je ziet kan de krijger dingen die de andere twee karakters niet kunnen, denk aan het afweren van aanvallen blockAttack() en het provoceren van vijanden om de krijger aan te vallen tauntEnemy(). Dit is logisch want deze dappere krijger is een vechter die vooraan staat en zijn of haar groepsleden beschermt tegen aanvallen van de vijand. Zo ook met de andere karakters die hun eigen specialismen hebben. Alleen de tovenaar kan teleporteren teleport() en anderen genezen met magische spreuken heal(), en alleen de schurk kan sloten openen pickLock() en vallen plaatsen placeTrap(). Het is dus goed dat we hier 3 verschillende klassen voor gemaakt hebben. Echter, valt het je misschien ook op dat sommige elementen in de klassen overeenkomen. Alle karakters kunnen blijkbaar bewegen move(), eten eat() en schatten plunderen loot(). En ook bij de attributen zul je iets soortgelijks zien. Alle karakters hebben blijkbaar levenspunten lifePoints, maar alleen de tovenaar heeft ook magische energie die in dit soort spellen mana genoemd wordt mana.

Het is aannemelijk dat het plunderen van de schatten, en wellicht ook het bewegen en eten voor alle karakters op dezelfde manier werkt en daarom op dezelfde manier geïmplementeerd dient te worden. Het lastige van bovenstaand klassendiagram is dan dat deze operaties op 3 verschillende plaatsen staan. Ik zou dit gemakkelijk kunnen oplossen door de implementatie te kopiëren en plakken, maar het nadeel daarvan is dat ik geen hergebruik toepas. Stel er komt in de toekomst nog een ander karakter bij, dan dien ik wederom netjes alle code te kopiëren en plakken en misschien vergeet ik dan wel een klein stukje en werkt het bij het nieuwe karakter net anders. Daarnaast is het wijzigen van deze code erg frustrerend. Stel dat het spel een beetje verandert en het plunderen van schatten moet aangepast worden. Nu moet ik 3 verschillende klassen langs om drie keer hetzelfde aan te passen. Waarschijnlijk lukt me dat wel, maar misschien ben ik even afgeleid door mijn telefoon of een collega en implementeer ik het per ongeluk bij 1 van de karakters net even iets anders dan bij de rest. Zie hier de mogelijke aanleidingen van een heleboel vervelende bugs.

Generaliseren

Het achterliggende idee van overerving is dat je de groep klassen kunt generaliseren. Je kunt aan deze groep een algemenere naam toekennen waaraan alle klassen voldoen. In ons spel kun je zeggen dat alle krijgers, tovenaars en schurken, ook karakters zijn. Een krijger is een karakter, maar ook een tovenaar is een karakter en ook een schurk is een karakter. We kunnen dit in het klassendiagram opnemen en zeggen dat karakter Character de generalisatie is van de 3 eerder genoemde klassen. Hieronder het aangepaste klassendiagram. Let op, dit diagram is nog niet goed, we gaan er strak mee verder.

full fig.2: De klasse Character toegevoegd als generalisatie van de drie eerdere klassen.

Door deze generalisatie werken de specifieke klassen nu een klein beetje anders. Het is namelijk zo dat de specifieke klassen nu ook alle elementen van de generieke klasse krijgen. Een krijger is dus alles dat gespecificeerd wordt in de klasse Warrior maar daarbij alles dat gespecificeerd wordt in de klasse Character. En zo ook voor de tovenaar die alles is dat gespecificeerd wordt in de klasse Sorcerer plus alles in Character. En uiteraard de schurk die Rogue en Character samen is. Dus als ik nu een operatie toevoeg aan de klasse Character dan krijgen automatische alle drie de specifieke klassen deze operatie ook. Ik voeg op deze wijze praten speak() toe aan alle karakters, want alle karakters kunnen praten.

full fig.3: Het klassendiagram met de operatie voor praten speak() toegevoegd, echter doordat deze operatie in de klasse Character staat, krijgen alle specifieke karakters de operatie automatisch ook, en kunnen allen dus praten.

Met deze informatie in gedachte valt het je misschien op dat sommige van de elementen die nu in de specifieke klassen staan, eigenlijk beter in de klasse Character kunnen staan, met als argument dat de implementatie daarvan voor alle specifieke klassen gelijk is. Eerder noemden we de operaties move(), eat() en loot(), maar hetzelfde geldt ook voor het attribuut lifePoints. Hieronder het aangepaste klassendiagram.

full fig.4: Alle overeenkomende elementen uit de drie specifieke klassen zijn verplaatst naar de algemene klasse Character.

Abstractie

In bovenstaand voorbeeld hebben we een klasse toegevoegd die eigenlijk niet in het conceptueel model voorkomt, namelijk de klasse voor karakter Character. Het is dus misschien geen echt concept, of in een andere discussie, zou je kunnen zeggen dat deze klasse misschien het werkelijke concept is, en dan zijn mogelijk de specifieke klassen geen nieuwe concepten omdat zij allen het concept karakter zijn. Hoe dan ook is het belangrijk dat je inziet dat niet zowel de nieuwe klasse voor "karakter" als de drie specifieke karakters concepten zijn want het zijn uiteraard geen twee verschillende dingen. Je dient je in een dergelijk geval af te vragen of er in het informatiesysteem objecten kunnen voorkomen van deze klassen. Een object van het type Warrior lijkt me zeer aannemelijk, en van Sorcerer en Rogue ook, maar een object van het type Character is niet realistisch. Het is niet mogelijk een object te maken dat alleen een karakter is, maar niet tevens 1 van onze specifieke karakters. Je bent of een krijger en daarmee een karakter, of een tovenaar en daarmee een karakter of een schurk en daarmee een karakter, maar niet alleen een karakter. We moeten er dus voor zorgen dat er van de klasse Character geen objecten gemaakt kunnen worden. We doen dit door deze klasse abstract te maken, wat als gevolg heeft dat er van deze klasse geen instanties (objecten) aangemaakt kunnen worden.

full fig.5: De klasse Character is abstract gemaakt om te zorgen dat er van deze klasse geen objecten gemaakt kunnen worden.

In UML geef je aan dat een klasse abstract is door de naam van de klasse schuingedrukt weer te geven. Omdat dit soms niet zo heel goed te zien is, en het al dan niet abstract zijn van een klasse essentiële informatie is voor de implementerende softwareontwikkelaar, wordt er meestal nog de commentaartekst {abstract} toegevoegd om de zichtbaarheid te verhogen.

Polymorfisme

Naast dat je met overerving generalisatie kunt toepassen, zoals hierboven omschreven, is het ook mogelijk om polymorfisme toe te passen. Het is namelijk mogelijk om in de algemene klasse een operatie te definiëren die in deze klasse geen implementatie krijgt. Het idee hiervan is dat de specifieke klassen deze implementatie dan dienen te verzorgen. Bij ons voorbeeld zou je je kunnen voorstellen dat alle karakters kunnen aanvallen en een operatie attack() kunnen toevoegen aan de klasse Character. Dit is een goed idee want inderdaad is het zo dat alle karakters in dit spel kunnen aanvallen. Het probleem van deze aanpak is echter dat de manier waarop het karakter aanvalt verschillend is. De krijger zal wellicht met een zwaard beginnen te slaan, waar de tovenaar misschien vuurballen schiet, en wie weet hoe de schurk zal gaan aanvallen? Je zou nu kunnen aannemen dat aanvallen blijkbaar niet overeenkomstig is tussen deze drie klassen en het aanvallen specifiek in elke klasse opnemen. Het nadeel daarvan is dat je de stelling "elk karakter kan aanvallen" teniet gedaan hebt, want dit wordt dan niet langer afgedwongen door de algemene klasse Character. Je wilt dus dat de operatie wel in de klasse Character gedefinieerd wordt, en dus aanwezig is, maar niet in deze klasse geïmplementeerd wordt want dat moet elke specifieke klasse zelf regelen. We noemen dit een abstracte operatie.

full fig.6: Doordat de operatie attack() in de klasse Character staat, kan elk karakter aanvallen, echter doordat deze operatie abstract is, moet elke specifieke klasse deze operatie van een eigen implementatie voorzien.

Omdat deze operatie in uitvoer verschillende verschijningsvormen heeft, te weten slaan met een zwaard of het schieten van een vuurbal of nog anders, voldoet deze operatie aan polymorfisme. De operatie kan aangeroepen worden op de abstracte klasse Character en voorziet dan in 1 uit meerdere verschijningsvormen afhankelijk van de specifieke implementatie van de afgeleide klasse.

Conclusie

Met overerving kun je overeenkomsten tussen een groep klassen op een centrale plek implementeren, zodat deze gemakkelijk kunnen worden hergebruikt. Daarnaast levert het centraal implementeren voordelen op bij het onderhouden van de software omdat wijzigingen maar op 1 plek gemaakt hoeven te worden om ze op alle plaatsen door te voeren. Het centraliseren van overeenkomstigheden noemen we ook generaliseren. Er wordt een algemene klasse gemaakt die de overeenkomstigheden vasthoudt en alle specifieke klassen worden van deze algemene klasse afgeleid. Het is belangrijk om deze algemene klasse vervolgens als abstract te markeren zodat hiervan geen objecten gemaakt kunnen worden in het informatiesysteem. Daarnaast is het mogelijk om in de algemene klasse operaties als abstract te markeren zodat ze wel aanwezig zijn in de algemene klasse, maar geen algemene implementatie krijgen. Dit laatste zorgt ervoor dat alle afgeleide klassen in een eigen implementatie dienen te voorzien.