Foundations for Professionals .NET Professionals im Profil guide to C# guidgen.de

Blog

Nutze den Augenblick
und teile der Welt mit, was Du zu sagen hast.

Vererben oder aggregieren?

Donnerstag, 1. Oktober 2009, 09:37 Uhr
Permalink | Kommentare (2) | Kommentare als RSSRSS

Am 13. Oktober 2008 haben Peter Bucher und ich unter dem Titel Noch Fragen, Bucher? Ja, Roden! angekündigt, jeweils zum ersten eines jeden Monats einen Kommentar zu einem vorab gemeinsam gewählten Thema verfassen zu wollen. Bisher sind in dieser Reihe folgende Kommentare erschienen:

Heute, am 1. Oktober 2009, ist es nun wieder so weit, und unser Thema für diesen Monat lautet:

Vererben oder aggregieren?

So wohl Peter wie auch ich haben uns unabhängig voneinander im Vorfeld unsere Gedanken gemacht, wie wir diesem Thema gegenüberstehen. Peters Kommentar findet sich zeitgleich in seinem Blog, folgend nun mein Kommentar zu diesem Thema:

In der Welt der modernen Softwareentwicklung haben die objektorientierten Sprachen in den vergangenen Jahrzehnten die primäre Rolle eingenommen – von Sprachen wie C++, Java und C# bis hin zu neuen Entwicklung wie Scala: All diese Sprachen unterstützen die objektorientierte Programmierung, teilweise basieren sie sogar gänzlich darauf.

Auch die Literatur hat sich dieser Tendenz angenommen: Kein Einsteiger lernt heute zu programmieren, ohne nicht zumindest einige wenige Schritte in der objektorientierten Programmierung gewagt zu haben. Doch wie wird Objektorientierung üblicherweise vermittelt?

In der Regel werden zunächst die Begriffe Klasse und Objekt erläutert, um sich im Anschluss daran an fortgeschrittene Themen wie Vererbung und generische Typen zu wagen. Prinzipiell ist daran auch nichts auszusetzen – wenn an dieser Stelle nicht in der Regel auch Schluss wäre.

Für die objektorientierte Programmierung stellt Vererbung zwar ein Schlüsselkonzept dar, jedoch wird es in der Praxis bei weitem nicht dermaßen häufig eingesetzt wie einem die Theorie weismachen will: Der Grund hierfür liegt schlichtweg darin, dass Vererbung für zahlreiche Szenarien wenn überhaupt, dann nur bedingt geeignet ist.

Als Begründung hierfür sei das Liskovsche Substitutionsprinzip angeführt, das im Jahr 1993 von Barbara Liskov und Jeanette Wing formuliert wurde, und auch den von Clean Code Developer propagierten Prinzipien angehört.

Frei formuliert besagt dieses Prinzip, dass abgeleitete Typen das gleiche Verhalten aufweisen müssen wie der Typ, von dem sie abgeleitet wurden. Obwohl diese Forderung zunächst trivial und logisch erscheint, wird sie in der Praxis häufig verletzt: Nämlich dann, wenn der abgeleitete Typ die Funktionalität nicht erweitert, sondern einschränkt.

Das Standardbeispiel für eine Verletzung des Liskovschen Substitutionsprinzips ist die Ableitung einer Kreis- von einer Ellipsenklasse – oder analog dazu die Ableitung einer Quadrat- von einer Rechteckklasse. In beiden Fällen schränkt die abgeleitete Klasse die Möglichkeiten zur Manipulation des Objekts semantisch ein.

Auf Grund der Anschaulichkeit und der Verbreitung dieses Beispiels trägt es sogar einen eigenen Namen, so ist es nämlich als Kreis-Ellipse-Problem bekannt.

Ein Beispiel für den gelungenen Einsatz von Vererbung stellen hingegen die diversen grafischen Steuerelemente in .NET dar. Unabhängig davon, welcher technologische Teilbereich betrachtet wird (ASP.NET, Silverlight, WPF, …), das Prinzip ist immer das gleiche: Alle grafischen Steuerelemente sind von einer gemeinsamen Basisklasse Control abgeleitet, die alle allgemeinen Eigenschaften und Methoden definiert.

Generell gilt jedoch, dass Vererbung sparsam eingesetzt werden sollte: Gerade weil es dermaßen leicht ist, eine Klasse von einer anderen abzuleiten, sollte man als Entwickler besondere Vorsicht walten lassen.

Was aber, wenn Vererbung prinzipiell tatsächlich Sinn ergeben würde, der abgeleitete Typ jedoch in seiner Funktionalität eingeschränkt werden müsste?

Als Beispiel hierfür sei eine Rechnungsverwaltung genannt, in der Rechnungen zwar hinzugefügt werden können, aus rechtlichen Gründen jedoch nicht mehr gelöscht werden dürfen, wenn sie erst einmal gebucht sind.

Sicherlich bietet es sich an, zur Datenhaltung der Rechnungen von dem generischen Typ List<T> abzuleiten, schließlich erhält man damit auf einfache Weise nicht nur typsicheren Zugriff auf die einzelnen Rechnungen, sondern erhält quasi zum Nulltarif auch diverse Methoden zum Verwalten, Durchsuchen und Sortieren dieser Liste.

Problematisch ist jedoch, dass nun auch eine Methode Remove zur Verfügung steht, mit der einmal gebuchte Rechnungen wieder entfernt werden könnten – die Aufgabe lautet also, diese Möglichkeit zu eliminieren. Da es in der Vererbung keine Möglichkeit gibt, eine Methode der Basisklasse in einer abgeleiteten Klasse zu entfernen, gibt es de facto nur zwei Möglichkeiten:

  • Zum einen kann die Methode überschrieben werden. In diesem Fall gibt es wiederum zwei Varianten, denn einerseits kann die Methode schlichtweg mit einer leeren Methode überschrieben werden, andererseits könnte in der überschriebenen Variante auch eine NotImplementedException geworden werden. In beiden Fällen wäre das Problem insofern gelöst, dass die Remove-Methode zwar noch aufgerufen werden kann, der Aufruf jedoch keine Auswirkungen auf die Liste hat.
  • Zum anderen kann auf Vererbung verzichtet werden. In diesem Fall stellt sich aber die Frage, wie die Datenstruktur implementiert werden soll – schließlich kann die Lösung aus naheliegenden Gründen auch nicht lauten, dass für solche Fälle eine eigene Liste entwickelt werden muss.

Die Lösung lautet: Es sollte ein eigener Datentyp kreiert werden, der eine Liste als internen Datenspeicher enthält – den Zugriff darauf jedoch über eigene Methoden reguliert. Dieses Verfahren wird als Aggregation bezeichnet, An die Stelle einer is-a-Beziehung tritt also eine is-part-of-Beziehung.

Welche Variante sich für welchen Fall empfiehlt, hängt vom jeweiligen konkreten Kontext ab. Als Faustregel kann jedoch gelten: Wenn der abgeleitete Typ den Basistyp ausschließlich erweitert, sich in semantischer Hinsicht wie der zugehörige Basistyp verhält und es sich tatsächlich um eine is-a-Beziehung handelt, dann kann Vererbung in Betracht gezogen werden.

Im Zweifelsfall wie auch für alle anderen Fälle empfiehlt sich jedoch der Einsatz von Aggregation.