Vererbung dargestellt mittels UML. Die abgeleitete Klasse hat die Attribute x und y und verfügt über die Methoden a und b (im UML-Sprachgebrauch Operationen a und b).
Die Vererbung (englischinheritance) ist eines der grundlegenden Konzepte der Objektorientierung und hat große Bedeutung in der Softwareentwicklung. Die Vererbung dient dazu, aufbauend auf existierenden Klassen neue zu schaffen, wobei die Beziehung zwischen ursprünglicher und neuer Klasse dauerhaft ist. Eine neue Klasse kann dabei eine Erweiterung oder eine Einschränkung der ursprünglichen Klasse sein. Neben diesem konstruktiven Aspekt dient Vererbung auch der Dokumentation von Ähnlichkeiten zwischen Klassen, was insbesondere in den frühen Phasen des Softwareentwurfs von Bedeutung ist. Auf der Vererbung basierende Klassenhierarchien bzw. Vererbungssysteme[1] spiegeln strukturelle und verhaltensbezogene Ähnlichkeiten der Klassen wider.
Die vererbende Klasse wird meist Basisklasse (auch Super-, Ober- oder Elternklasse) genannt, die erbende abgeleitete Klasse (auch Sub-, Unter- oder Kindklasse). Den Vorgang des Erbens nennt man meist Ableitung oder Spezialisierung, die Umkehrung hiervon Generalisierung, was ein vorwiegend auf die Modellebene beschränkter Begriff ist. In der Unified Modeling Language (UML) wird eine Vererbungsbeziehung durch einen Pfeil mit einer dreieckigen Spitze dargestellt, der von der abgeleiteten Klasse zur Basisklasse zeigt. Geerbte Attribute und Methoden werden in der Darstellung der abgeleiteten Klasse nicht wiederholt.
Abgeleitete Klasse und Basisklasse stehen typischerweise in einer „ist-ein“-Beziehung zueinander.
Klassen dienen der Spezifikation von Datentyp und Funktionalität, die beide vererbt werden können.
Einige Programmiersprachen trennen zumindest teilweise zwischen diesen Aspekten und unterscheiden zwischen Schnittstelle (englisch Interface) und Klasse.
Wenn eine abgeleitete Klasse von mehr als einer Basisklasse erbt, wird dies Mehrfachvererbung genannt. Mehrfaches Erben ist nicht bei allen Programmiersprachen möglich, bei manchen nur in eingeschränkter Form.
Das folgende Beispiel stellt einen möglichen Ausschnitt aus dem Anwendungsgebiet der Unterstützung eines Fahrzeugverleihs dar. Die Basisklasse Fahrzeug enthält die Attribute Fahrzeugnummer, Leergewicht und ZulässigesGesamtgewicht. Die Spezialisierungen Kraftfahrzeug und Fahrrad ergänzen weitere Attribute zu den von der Basisklasse geerbten Gewichtsangaben und der identifizierenden Fahrzeugnummer. In jedem Objekt der Klasse Kraftfahrzeug werden also die Attribute Fahrzeugnummer, Leergewicht, ZulässigesGesamtgewicht, Höchstgeschwindigkeit und Leistung gespeichert. In der Klasse Fahrzeug gibt es die Methode PruefeVerfuegbarkeit, die unter Verwendung der Fahrzeugnummer die Verfügbarkeit eines bestimmten Fahrzeugs ermittelt, beispielsweise durch einen Datenbankzugriff. Diese Methode wird von allen Spezialisierungen geerbt und ist somit für diese ebenfalls nutzbar. Dasselbe gilt auch dann, wenn nachträglich eine weitere Methode in der Klasse Fahrzeug ergänzt wird, beispielsweise eine Methode HatNavigationssystem, wenn ein Teil der Fahrzeuge mit Navigationssystemen ausgestattet wird.
In der Klasse Kraftfahrzeug wird die MethodePrüfeFahrerlaubnis eingeführt. Diese Methode soll bei Übergabe einer konkreten Fahrerlaubnis ermitteln, ob das durch ein Objekt dieser Klasse repräsentierte Fahrzeug mit dieser Fahrerlaubnis geführt werden darf.[A 1] Die Fahrerlaubnis könnte länderspezifisch sein, zudem müssen die Länder berücksichtigt werden, in denen das Kraftfahrzeug betrieben werden soll. Auf Basis der Attribute Höchstgeschwindigkeit und Leistung können möglicherweise bereits in der Klasse Kraftfahrzeug einige Implementierungen vorgenommen werden, wenn für alle betreibbaren Fahrzeuge eines Landes die Eignung der Fahrerlaubnis bereits anhand des zulässigen Gesamtgewichts, der Höchstgeschwindigkeit und der Leistung entscheidbar ist. Viele Fälle sind aber erst auf Ebene der Spezialisierungen Motorrad, PKW und LKW entscheidbar, so dass diese Methode in diesen Klassen überschrieben werden muss. Beispielsweise ist die Anzahl der Sitzplätze des Kraftfahrzeugs in manchen Fällen zu berücksichtigen.
Schematisches Speicherabbild eines Objekts der Klasse PKW
Innerhalb eines dieses Modell implementierenden Anwendungsprogramms würde zur Prüfung, ob eine Fahrerlaubnis gültig ist, nach Eingabe der entsprechenden Fahrzeugdaten das konkrete, zu mietende Fahrzeug instantiiert, das heißt die entsprechende Spezialisierung.
Zudem würde ebenfalls ein Objekt für die vorliegende Fahrerlaubnis erzeugt. Dieses würde der Methode PrüfeFahrerlaubnis des Fahrzeug-Objekts übergeben und von dieser ausgewertet. Das Ergebnis der Überprüfung könnte beispielsweise dem Sachbearbeiter angezeigt werden. Der Aufbau des Speicherabbilds ist in nebenstehender Abbildung schematisch für ein Objekt der Klasse PKW dargestellt. Die aus den verschiedenen Klassen geerbten Attribute liegen dabei typischerweise direkt hintereinander. Weiterhin enthält das Speicherabbild eines Objekts einen Zeiger auf die sogenannte Tabelle virtueller Methoden, die der Ermittlung des Einsprungpunkts der konkreten Implementierung bei einem Methodenaufruf dient.[3]
In der ProgrammierspracheC++ könnte die Implementierung der KlassenFahrzeug, Kraftfahrzeug, Fahrrad, Motorrad, PKW, LKW folgendermaßen aussehen:
Die Vererbung ermöglicht bei der Softwareentwicklung eine Modellierung, die der menschlichen Vorstellung der realen Welt sehr nahe kommt. Es gibt sehr unterschiedliche Anwendungen des Vererbungsmechanismus. Nach wie vor ist umstritten, ob die Vererbung nur für sehr eng begrenzte Anwendungsbereiche verwendet werden sollte und ob ein Einsatz mit der hauptsächlichen Intention des Wiederverwendens von Code der Softwarequalität eher abträglich ist.[4][5]
Folgende Anwendungskontexte werden empfohlen oder tauchen in der Praxis auf:[5][6]
Subtyp-Vererbung: Bei dieser Art der Vererbung ist die erbende Klasse ein Subtyp der Basisklasse im Sinne eines abstrakten Datentyps. Dies bedeutet, dass ein Objekt des Subtyps an jeder Stelle eingesetzt werden kann, an der ein Objekt des Basistyps erwartet wird. Die Menge der möglichen Ausprägungen des Subtyps stellt eine Teilmenge derer des Basistyps dar.
Vererbung zur Erweiterung: In der abgeleiteten Klasse wird neue Funktionalität gegenüber der Basisklasse ergänzt. Diese Variante der Vererbung stellt einen scheinbaren Widerspruch zur einschränkenden Subtyp-Vererbung dar. Die Erweiterung bezieht sich dabei aber auf zusätzliche Attribute.[A 2] Diese Variante beinhaltet auch Anpassungen durch Überschreiben von Methoden, um beispielsweise Funktionalität zu ergänzen, die in der Basisklasse nicht relevant ist. Auch schließt diese Variante den Fall ein, dass nur ein Teil der Funktionalität einer abstrakten Klasse in der abgeleiteten – in diesem Fall ebenfalls abstrakten – Klasse implementiert wird, und zusätzlich erforderliche Implementierungen weiteren Spezialisierungen vorbehalten bleiben (Reification).
Vererbung zur Unterstützung allgemeiner Fähigkeiten: Bei dieser Variante geht es darum, die Unterstützung von Basisfunktionalität einer Anwendungsarchitektur oder Klassenbibliothek zu etablieren. Eine Basisfunktionalität wie Serialisierbarkeit oder Vergleichbarkeit wird dabei durch eine abstrakte Klasse (Schnittstelle) deklariert – typische Bezeichner sind Serializable[7] und Comparable.[8] Die Implementierung aller Anforderungen der Schnittstelle muss in der abgeleiteten Klasse erfolgen. Formal entspricht diese Art der Vererbung der Subtyp-Vererbung.
Vererbung von Standardimplementierungen: Allgemeine für mehrere Typen verwendbare Funktionalität wird dabei in zentralen Klassen implementiert. Diese Variante dient der zweckdienlichen Wiederverwendung allgemeiner Programmteile (Mixin-Klasse).
Es gibt auch Verwendungen der Vererbung, die als nicht sinnvoll angesehen werden. Insbesondere bei den ersten Gehversuchen in objektorientierter Programmierung ergibt sich häufig eine aus der Begeisterung resultierende übertriebene Abstufung der Vererbungshierarchie, oft für eine simple zusätzliche Eigenschaft. Beispielsweise dürften für eine Klasse Person die Spezialisierungen WeiblichePerson und MännlichePerson in den wenigsten Fällen zweckmäßig sein und bei der Modellierung der eigentlich relevanten Aspekte eher behindern. Eine weitere fragwürdige Verwendung ist, wenn die erbende Klasse nicht in einer „ist-ein“-, sondern in einer „hat“-Beziehung zur Basisklasse steht, und eine Aggregation angebracht wäre. Häufig tritt dieser Fall in Verbindung mit Mehrfachvererbung auf. Apfelkuchen als Erbe von Kuchen und Apfel stellt ein bildhaftes Beispiel dieses Modellierungsfehlers dar, da Apfel keine sinnvolle Basisklasse ist.[5]
Ungünstige Modellierung
Modellierung mittels Rollen
Beim Übergang von der objektorientierten Modellierung zur Programmierung gibt es die Situation, dass die Modellierung einer klassifizierenden Hierarchie der fachlichen Anwendungsobjekte nicht ohne weiteres auf die Programmebene übertragen werden kann. Beispielsweise mag aus konzeptioneller Sicht die Modellierung von Kunde und Mitarbeiter als Spezialisierungen von Person sinnvoll erscheinen. Auf Ebene der Programmierung ist eine solche Klassifizierung zum einen statisch – das heißt, eine Person kann programmtechnisch nicht ohne weiteres von der Rolle des Mitarbeiters zur Rolle des Kunden wechseln. Zum anderen kann eine Person auf diese Weise auch nicht mehrere Rollen gleichzeitig einnehmen. Dass letzteres nicht sinnvoll durch Hinzufügen einer mehrfach erbenden, weiteren Spezialisierung KundeUndMitarbeiter gelöst werden kann, wird beispielsweise bei Hinzunahme einer weiteren Rolle Lieferant deutlich. Die übliche Lösung ist die Trennung der Aspekte und die Modellierung einer assoziativen Beziehung zwischen Person und ihren Rollen.[9]
Varianten der Vererbung von Typ und Implementierung
Mittels Vererbung können sowohl der Typ, der durch seine Schnittstelle spezifiziert wird, als auch die Funktionalität an die abgeleitete Klasse weitergegeben werden. Die Konsequenzen dieser Doppelfunktion der Vererbung werden seit Jahren kontrovers diskutiert.[4][5] Auch neuere Programmiersprachen wie Java oder .NET-Sprachen wie C# und VB.NET unterstützen keine durchgängige Trennung dieser Vererbungsvarianten, bieten jedoch für Schnittstellen(interface) und Klassen (class) zwei formal getrennte Konzepte an. Es lassen sich drei Fälle unterscheiden:[10]
Vererbung von Typ und Implementierung (meist Implementierungsvererbung oder einfach nur Vererbung genannt, engl. Subclassing)
Vererbung des Typs (meist als Schnittstellenvererbung bezeichnet, engl. Subtyping)
Reine Vererbung der Implementierung (in Java oder .NET-Sprachen nicht direkt möglich)
Bei der letzten Variante stehen abgeleitete Klasse und Basisklasse nicht in einer „ist-ein“-Beziehung zueinander.
Implementierungsvererbung
Hierbei wird von der Basisklasse die Implementierung und implizit auch deren Schnittstelle geerbt. Die abgeleitete Klasse übernimmt dabei die Attribute und Funktionalität der Basisklasse und wandelt diese gegebenenfalls ab oder ergänzt diese um weitere für diese Spezialisierung zusätzlich relevante Eigenschaften. Auch wenn nachträglich Funktionalität der Basisklasse ergänzt oder verbessert wird, profitiert die abgeleitete Klasse davon.
Im Folgenden wird in der Programmiersprache Java ein Beispiel für die Ableitung von Quadrat als Spezialisierung von Rechteck skizziert. Dieses Beispiel findet sich in ähnlicher Form häufig in der Literatur und ist zur Veranschaulichung vieler – auch ungünstiger – Aspekte hilfreich, kann aber eigentlich nicht als besonders gutes Beispiel der Vererbung gelten.
Die Klasse Rechteck besitzt die Attribute laenge und breite, die über den Konstruktor gesetzt werden. Daneben gibt es Methoden (Funktionen) zur Berechnung des Umfangs und der Länge der Diagonalen des Rechtecks. Die Spezialisierung Quadrat erbt diese Funktionalität (Schlüsselwortextends). Der Konstruktor für Quadrat erfordert nur noch einen statt zwei Parameter, da Länge und Breite ja übereinstimmen. Die in der Klasse Rechteck implementierten Berechnungen von Umfang und Diagonalenlänge stimmen auch für das Quadrat. In diesem Beispiel wird dennoch zur Veranschaulichung – aus Optimierungsgründen – eine Modifikation der Berechnung der Diagonalenlänge vorgenommen, da diese bei einem Quadrat auf einfachere Weise berechnet werden kann. Die Berechnung des Umfangs wird nicht reimplementiert, sondern von der Basisklasse übernommen – obwohl natürlich auch dort eine geringfügige Vereinfachung möglich wäre.
publicclassQuadratextendsRechteck{// Einmalige Berechnung der Wurzel aus 2privatestaticfinaldoubleWURZEL2=Math.sqrt(2);publicQuadrat(doublelaenge){// Aufruf des Konstruktors der Basisklassesuper(laenge,laenge);}@OverridepublicdoublegetDiagonale(){returnWURZEL2*getLaenge();}}
Schnittstellenvererbung
In der Softwareentwicklung gab es seit den 1970er Jahren zwei parallele Entwicklungen, eine davon mündete in die objektorientierte Programmierung, andererseits wurden algebraische Spezifikationsmethoden zur Unterstützung des Softwareentwurfs entwickelt. Ein Vorteil solcher Spezifikationen ist, dass sie mit einer mathematischen Semantik versehen werden können.[11] Ein wesentliches Ergebnis dieser Bestrebungen war das Konzept des abstrakten Datentyps, das die Spezifikation eines Datentyps unabhängig von der Implementierung zum Ziel hat. Klassen, genau genommen deren Schnittstellen, gelten als das Abbild eines abstrakten Datentyps. Hierbei ist aber eigentlich unpassend, dass bei Vererbung praktisch von keiner Sprache[12] eine durchgängige Trennung der Vererbung von Schnittstelle und Implementierung explizit unterstützt wird. Relativ neue Sprachen wie Java und .NET-Sprachen führen zwar mit den Schnittstellen (Interfaces) ein Konzept zur Abbildung abstrakter Datentypen ein, unterstützen aber keine durchgängige Trennung, denn ist eine Schnittstelle einmal von einer Klasse implementiert, erbt jede weitere Spezialisierung sowohl die Implementierung als auch die Schnittstelle.[4] Spezialisten für die objektorientierte Programmierung, beispielsweise Bertrand Meyer, sehen in einer vollständigen Aufspaltung mehr Schaden als Nutzen.[5] Ein Grund ist, dass die Nähe von Schnittstelle und Implementierung im Programmcode das Verständnis und die Wartbarkeit erleichtert.[13]
In diesem Zusammenhang von Bedeutung ist auch das liskovsche Substitutionsprinzip. Dieses fordert, dass ein Subtyp sich so verhalten muss, dass jemand, der meint, ein Objekt des Basistyps vor sich zu haben, nicht durch unerwartetes Verhalten überrascht wird, wenn es sich dabei tatsächlich um ein Objekt des Subtyps handelt. Objektorientierte Programmiersprachen können eine Verletzung dieses Prinzips, das aufgrund der mit der Vererbung verbundenen Polymorphie auftreten kann, nicht von vornherein ausschließen. Häufig ist eine Verletzung des Prinzips nicht auf den ersten Blick offensichtlich.[10] Wenn etwa beim oben skizzierten Beispiel in der Basisklasse Rechteck zur nachträglichen Veränderung der Größe die Methoden setLaenge und setBreite eingeführt werden,[A 3] muss in der Klasse Quadrat entschieden werden, wie damit umzugehen ist. Eine mögliche Lösung ist, dass beim Setzen der Länge automatisch die Breite auf denselben Wert gesetzt wird und umgekehrt. Wenn eine Anwendung unterstellt, ein Rechteck vor sich zu haben, und bei Verdopplung der Länge eines Rechtecks eine Verdopplung der Fläche erwartet, überrascht bei einer Instanz des Typs Quadrat die durch automatische Angleichung der Breite verursachte Vervierfachung der Fläche.[A 4]
Die fehlende Trennung zwischen Typ- und Implementierungsvererbung führt in der Praxis häufig dazu, dass in der Schnittstelle einer Klasse Implementierungsdetails durchscheinen.[14] Eine Strategie zur Vermeidung dieses Effekts ist die Verwendung abstrakter Klassen oder Schnittstellen in den wurzelnahen Bereichen der Klassenhierarchie. Günstig ist, auf abstrakter Ebene möglichst weit zu differenzieren, bevor Implementierungen ergänzt werden. Eine solche auf Schnittstellen basierte Grundlage ist auch in Verbindung mit verteilten Architekturen wie CORBA oder COM notwendig.[13]
Reine Implementierungsvererbung
Bei der reinen Implementierungsvererbung, die auch als private Vererbung bezeichnet wird, nutzt die erbende Klasse die Funktionalität und Attribute der Basisklasse, ohne nach außen als Unterklasse dieser Klasse zu gelten. Als – etwas konstruiertes – Beispiel könnte eine Klasse RechtwinkligesDreieck von der Klasse Rechteck des obigen Beispiels die Implementierung erben, um die Hypotenuse über die Methode getDiagonale zu berechnen, nachdem die Länge der Katheten für Länge und Breite eingesetzt wurden.
Beispielsweise in C++ oder Eiffel gibt es die Möglichkeit einer reinen Implementierungsvererbung, in Java oder den .NET-Sprachen gibt es sie nicht. Eine Alternative bei letzteren Sprachen ist die Verwendung von Delegation, die einiges mehr Programmcode erfordert.[10]
Zusammenspiel der Methoden in der Klassenhierarchie
Wenn in einer Klasse eine Methodeüberschrieben wird, soll häufig nur Funktionalität ergänzt und die Implementierung der Basisklasse weiterhin genutzt werden, da diese bereits allgemeine Aspekte abdeckt, die für die Spezialisierung ebenfalls gültig sind. Hierfür ist es erforderlich, dass innerhalb der Methodenimplementierung der spezialisierten Klasse das Pendant der Basisklasse aufgerufen wird. Dieser Aufruf erfolgt typischerweise zu Beginn oder am Ende der überschreibenden Methode, teilweise ist aber auch zusätzliche Funktionalität vor und nach diesem Aufruf zu implementieren.[15]
Beispiel einer Aufrufkaskade
Die verschiedenen Programmiersprachen ermöglichen einen Aufruf der Basisklassenimplementierung auf unterschiedliche Weise. Die meisten Freiheitsgrade bietet C++, dort wird dem Methodennamen der Klassenname als Präfix vorangestellt (Scope-Operator). Dieses Verfahren geht über diesen Anwendungsfall weit hinaus, denn es ermöglicht den Aufruf jeder beliebigen Methode aller Klassen innerhalb der Klassenhierarchie. Etwas einschränkender ist beispielsweise Java, dort gibt es das Schlüsselwort super, das dem Methodennamen vorangestellt wird. Deutlich formaler ist der Aufruf der Basisklassenmethode beispielsweise in der Sprache CLOS gelöst: Dort wird allein durch das Schlüsselwort call-next-method die Basisimplementierung aufgerufen, ohne dass Methodenname oder Parameter spezifiziert werden, die aktuellen Parameter der spezialisierenden Methode werden implizit übergeben. Der formalere Ansatz ist weniger redundant und fehleranfällig, bietet dafür aber weniger Flexibilität.[15]
Anhand des einführenden Beispiels lässt sich eine solche Aufrufkaskade erläutern. Die Methode PruefeFahrerlaubnis gibt dabei zurück, ob die Prüfung durchgeführt werden konnte, und wenn dies der Fall ist, zusätzlich das Ergebnis dieser Prüfung. Die Implementierung der Klasse PKW ruft zunächst die Implementierung der Klasse Kraftfahrzeug auf, um die Fälle abzuhandeln, die anhand Höchstgeschwindigkeit, Leistung oder zulässigem Gesamtgewicht entscheidbar sind. Die Implementierung in Kraftfahrzeug wiederum delegiert die Prüfung des zulässigen Gesamtgewichts weiter an seine Basisklasse. Nach Rücksprung aus den gerufenen Basisimplementierungen wird die Prüfung jeweils fortgesetzt, wenn der Fall noch nicht entschieden werden konnte.
Mehrfachvererbung mit gemeinsamer indirekter Basisklasse (UML)
Um Mehrfachvererbung handelt es sich, wenn eine abgeleitete Klasse direkt von mehr als einer Basisklasse erbt. Ein sequentielles, mehrstufiges Erben wird dagegen nicht als Mehrfachvererbung bezeichnet. Ein sehr häufiger Anwendungsfall der Mehrfachvererbung ist die Verwendung von Mixin-Klassen, die allgemein verwendbare Implementierungen beisteuern und somit der Vermeidung von Redundanz dienen.[16]
Ein anderes Beispiel für Mehrfachvererbung ergibt sich durch die Erweiterung des einführenden Beispiels um die Klassen Schienenfahrzeug und Zweiwegefahrzeug. Letztere erbt dabei sowohl von Kraftfahrzeug als auch von Schienenfahrzeug und hat somit sowohl alle Attribute der Kraftfahrzeuge als auch das zusätzliche Attribut Spurweite, das von Schienenfahrzeug geerbt wird.
Die Notwendigkeit von Mehrfachvererbung ist umstritten, sie wird nicht von allen Sprachen unterstützt, beispielsweise nicht von Smalltalk. Die erste Sprache, die eine Mehrfachvererbung unterstützte, war Flavors, eine objektorientierte Erweiterung von Lisp. Eine umfassende Unterstützung bieten beispielsweise auch C++, Eiffel und Python. Java und .NET-Sprachen bieten eine eingeschränkte Unterstützung, dort kann eine Klasse zwar von beliebig vielen Schnittstellen, aber nur von einer Klasse erben, die Implementierungen enthält.[16] Eine andere Lösung hält Ruby bereit, dort ist ebenfalls nur eine direkte Basisklasse möglich, allerdings kann eine Klasse beliebig viele sogenannte Modules einbinden, was dem Grundgedanken einer Mixin-Vererbung direkt entspricht.[17]
Neben einem erheblichen zusätzlichen Implementierungsaufwand für Compiler und Laufzeitumgebung gibt es vor allem zwei Gründe für die häufige fehlende oder eingeschränkte Unterstützung:[18]
Mögliche Namenskollisionen bei geerbten Attributen oder Methoden
Mehrfaches Auftreten derselben Basisklasse im Vererbungsbaum
Für erstgenanntes Problem bieten die Sprachen meist Möglichkeiten der Umbenennung. Letztere Konstellation, die auch als Diamond-Problem bezeichnet wird, tritt nur bei Vererbung der Implementierung in Erscheinung. Hier kann es sowohl sinnvoll sein, dass das resultierende Objekt nur eine Instanz der mehrfach auftretenden Klasse enthält, als auch mehrere. Für das obige Beispiel des Zweiwegefahrzeugs bedeutet dies entweder das Vorhandensein von nur einer Instanz der Basisklasse Fahrzeug oder von deren zwei. C++ bietet über das Konzept sogenannter virtueller Basisklassen beide Möglichkeiten an.[18] Eiffel bietet auch beide Möglichkeiten und dies sogar auf Ebene einzelner Attribute und Methoden.[19] Das kann im skizzierten Beispiel sogar sinnvoll sein: Das Leergewicht ist bei einem Zweiwegefahrzeug grundsätzlich gleich, egal ob es auf der Schiene oder auf der Straße betrieben wird. Dies muss aber nicht unbedingt auch für das zulässige Gesamtgewicht gelten. Python hat zum Erzeugen einer sinnvollen Vererbungshierarchie ab Version 2.3 in solchen Fällen das Konzept der sogenannten C3-Linearisierung implementiert.
Multilevel-Vererbung
Bei der Objektorientierte Programmierung gibt es häufig das Problem, dass man unterschiedliche Klassen definiert hat, die untereinander nicht auf Variablen zugreifen können. Um in einer Klasse die Attribute und Methoden einer anderen Klasse sichtbar zu machen, kann man Vererbung nutzen. Von Multilevel-Vererbung spricht man ab wenigstens drei Klassen, die hintereinander geschaltet sind.[20] Besonders für Rapid-Prototyping eignet sich das Konzept.[21] Einerseits kann man in separaten Klassen den Programmcode verwalten, gleichzeitig hat man jedoch eine große Klasse. Multilevel-Vererbung kann mit Mehrfachvererbung kombiniert werden.[22]
Im Zusammenhang mit dem liskovschen Substitutionsprinzip steht auch die Behandlung der Varianz bei den Signaturen überschriebener Methoden. Viele Programmiersprachen ermöglichen keine Varianz, das heißt, die Typen der Methodenparameter überschriebener Methoden müssen exakt übereinstimmen. Dem liskovschen Prinzip entspricht die Unterstützung von Kontravarianz für Eingangs- und Kovarianz für Ausgangsparameter. Das bedeutet, Eingangsparameter können allgemeiner sein als bei der Basisklasse, der Typ des Rückgabewerts darf spezieller sein.[23]
Von wenigen Sprachen wird die Deklaration der Ausnahmen (englisch Exceptions) ermöglicht, die beim Aufruf einer Methode auftreten können. Die Typen der möglichen Ausnahmen gehören dabei zur Signatur einer Methode. Bei Java und Modula-3 – den beiden einzigen bekannteren Sprachen, die so etwas unterstützen – muss die Menge der möglichen Ausnahmetypen einer überschriebenen Methode eine Teilmenge der ursprünglichen Typen sein, was Kovarianz bedeutet und dem liskovschen Substitutionsprinzip entspricht.[24][A 5]
Im Zusammenhang mit dem liskovschen Substitutionsprinzip steht auch das Design-By-Contract-Konzept, das von Eiffel unterstützt wird. Dabei gibt es die Möglichkeit, Vor- und Nachbedingungen für Methoden sowie Invarianten für Klassen zu definieren. Die Klassenvarianten sowie die Nachbedingungen müssen dabei in Spezialisierungen gleich oder restriktiver sein, die Vorbedingungen können gelockert werden.[25]
Datenkapselung im Rahmen der Vererbung
Bei der Spezifizierung der Sichtbarkeit der Attribute und Methoden von Klassen (Datenkapselung) wird häufig unterschieden, ob der Zugriff beispielsweise durch eine abgeleitete Klasse oder „von außen“, das heißt bei einer anderweitigen Verwendung der Klasse, erfolgt. In den meisten Sprachen werden drei Fälle unterschieden:[26]
öffentlich (public): Die Eigenschaft ist ohne Einschränkungen sichtbar
geschützt (protected): Die Eigenschaft ist in der Klasse selbst und für abgeleitete Klassen sichtbar (auch mehrstufig), von außen hingegen nicht.
privat (private): Die Eigenschaft ist nur in der Klasse selbst sichtbar.
Nicht alle Sprachen unterstützen diese dreiteilige Gliederung. Manche Abgrenzungen der Sichtbarkeit sind auch anders ausgelegt. Java und die .NET-Sprachen führen zusätzlich noch Varianten ein, die die Sichtbarkeit auf sprachspezifische Untereinheiten der Programmstruktur (Package oder Assembly) begrenzen. In Ruby hat private eine abweichende Bedeutung: Auf private Eigenschaften kann auch von spezialisierenden Klassen zugegriffen werden. Allerdings ist grundsätzlich nur der Zugriff auf Eigenschaften derselben Instanz möglich.[27][A 6]
Ein weiterer, bei Vererbung relevanter Aspekt der Datenkapselung ist die Möglichkeit, in abgeleiteten Klassen die Sichtbarkeit von Eigenschaften gegenüber der Basisklasse zu verändern. Beispielsweise in C++ oder Eiffel ist es möglich, die Sichtbarkeit aller oder einzelner Eigenschaften beim Erben einzuschränken. In Java oder den .NET-Sprachen dagegen ist keine solche Änderung der Sichtbarkeit bei Vererbung möglich.[26]
Typkonvertierung bei statischer Typisierung
Programmiersprachen lassen sich in solche mit statischer oder dynamischer Typisierung einteilen. Bei dynamischer Typisierung wird für Variablen und Parameter nicht explizit ein Typ festgelegt. Smalltalk war die erste objektorientierte Sprache mit dynamischer Typisierung. Bei statischer Typisierung dagegen wird – meist durch eine Deklaration wie beispielsweise in Java – kenntlich gemacht, welchen Typ der Wert einer Variablen oder eines Parameters aufweisen muss. Bei Zuweisung oder Parameterübergabe kann die Zuweisungskompatibilität der Typen in diesem Fall bereits während der Übersetzungszeit geprüft werden.[28]
An jeder Stelle, an der ein Objekt einer bestimmten Klasse erwartet wird, kann auch ein Objekt verwendet werden, das einer Spezialisierung dieser Klasse angehört. Beispielsweise kann eine Variable des Typs PKW immer einer Variable des Typs Kraftfahrzeug zugewiesen werden. Allerdings sind nach einer solchen Zuweisung die zusätzlichen Eigenschaften der Spezialisierung, im Beispiel die Anzahl der Sitzplätze, nicht direkt zugänglich. Das Objekt der Basisklasse verhält sich jedoch beim Aufruf von virtuellen Methoden wie ein Objekt der spezialisierenden Klasse.[A 7] Eine solche Konvertierung wird Upcast genannt.[29]
Das Gegenstück dazu, ein Downcast, ist problematischer, jedoch in einigen Fällen notwendig. Auch statisch typisierende Sprachen ermöglichen meist eine solche Konvertierung, die aber explizit veranlasst werden muss. In diesem Fall ist auch bei statisch typisierenden Sprachen erst zur Laufzeit überprüfbar, ob ein Objekt tatsächlich den geforderten Typ aufweist.[10] Ein solcher Downcast, beispielsweise von Kraftfahrzeug zu PKW, ist nur sinnvoll, wenn sichergestellt ist, dass das Objekt tatsächlich vom Typ der konkreten Spezialisierung ist. Wird keine Prüfung durchgeführt und in diesem Beispiel ein Objekt, das einen LKW repräsentiert, in den Typ PKW konvertiert, wird im Regelfall eine Ausnahme erzeugt.
Programmiersprachen mit zentraler Basisklasse
Viele objektorientierte Programmiersprachen verfügen über eine zentrale Klasse, von der alle Klassen – über wie viele Stufen auch immer – letztlich abgeleitet sind. Beispielsweise gibt es in Ruby, Java und bei .NET eine solche Klasse. Diese heißt bei diesen Sprachen Object. In Eiffel wird sie mit ANY bezeichnet. Zu den wenigen Ausnahmen, in denen es keine solche Klasse gibt, zählen C++ oder Python.
In den Sprachen mit zentraler Basisklasse erbt eine Klasse, für die keine Basisklasse angegeben wird, implizit von dieser besonderen Klasse. Ein Vorteil davon ist, dass allgemeine Funktionalität, beispielsweise für die Serialisierung oder die Typinformation, dort untergebracht werden kann. Weiterhin ermöglicht es die Deklaration von Variablen, denen ein Objekt jeder beliebigen Klasse zugewiesen werden kann. Dies ist besonders hilfreich zur Implementierung von Containerklassen, wenn eine Sprache keine generische Programmierung unterstützt.[A 8]
Dieses Verfahren hat allerdings den Nachteil, dass in einen solchen allgemeinen Container Objekte jeden Typs hinzugefügt werden können. Da beim Zugriff auf ein Objekt des Containers normalerweise ein spezieller Typ erwartet wird, ist deshalb eine Typumwandlung(Downcast) erforderlich. Die entsprechende Typprüfung kann jedoch erst zur Laufzeit erfolgen.[30]
Persistenz in Datenbanken
Neben einer einfachen Serialisierung ist die Speicherung in einer Datenbank das üblichste Verfahren, Objekte persistent zu machen. Objektorientierte Datenbanken haben das Ziel, einen sogenannten Impedance Mismatch zu vermeiden, der bei Abbildung der bei der Programmierung verwendeten Vererbungs- und Objektstruktur auf eine relationale Datenbank entsteht. Objektorientierte Datenbanken haben sich aber bis heute nicht durchgesetzt, so dass häufig sogenannte objektrelationale Mapper verwendet werden.[31]
Bei der objektrelationalen Abbildung der Vererbungsbeziehungen werden drei Möglichkeiten unterschieden:[32]
Die Attribute aller Klassen einer Hierarchie werden in einer Tabelle gespeichert (Single Table Inheritance)
Die Attribute jeder Klasse werden in einer separaten Tabelle gespeichert (Class Table Inheritance)
Die Attribute jeder nicht abstrakten Klasse werden in einer separaten Tabelle gespeichert (Concrete Table Inheritance)
Bei der ersten Variante (Single Table Inheritance) muss der Typ des Objekts in einer zusätzlichen Spalte gespeichert werden. Die Spalten der Attribute, die bei konkreten Objekten der Klassenhierarchie nicht vorhanden sind, enthalten Null-Werte. Beides ist bei den zwei letzten Varianten nicht nötig, die dritte Variante ist dabei eine Art Kompromiss.[32]
Bei echten objektorientierten Datenbanken werden im Wesentlichen zwei gegensätzliche Strategien unterschieden: Persistenz durch Vererbung (by Inheritance) und orthogonale Persistenz. Bei der Persistenz durch Vererbung hängt die Eigenschaft, ob ein Objekt transient oder persistent ist, vom Typ ab, und wird durch Erben von einer Klasse etabliert, die die Funktionalität zur Anbindung an die Datenbank bereitstellt. Bei orthogonaler Persistenz können Objekte derselben Klasse sowohl persistent als auch transient sein, die Eigenschaft ist also völlig unabhängig vom Typ.[33]
Vererbung im Kontext der Softwarewartung
Objektorientierte Elemente und dabei nicht zuletzt der Vererbungsmechanismus besitzen eine Ausdrucksstärke, die sich sehr positiv auf die Qualität und Verständlichkeit eines Systementwurfs auswirkt. Umfangreiche Klassenbibliotheken sind entstanden, deren Funktionalität mit Hilfe der Vererbung anwendungsspezifisch angepasst oder erweitert werden kann. Nicht zuletzt dank des Vererbungsmechanismus können Softwaresysteme modular aufgebaut werden, was die Beherrschbarkeit großer Systeme ermöglicht und beispielsweise auch Portierungen erleichtert.[34] Allerdings steigern unnötig tief verschachtelte Vererbungshierarchien die Komplexität und das Verständnis erheblich, was zu Fehlern bei Verwendung oder Änderung der Basisklassen führen kann.[35]
Neben den positiven Aspekten haben sich bei der objektorientierten Programmierung auch negative Aspekte im Hinblick auf die Softwarewartung gezeigt, die vor allem im Zusammenhang mit der Polymorphie, aber auch mit der Vererbung stehen.[36]
Ergänzung oder Anpassung einer Klassenschnittstelle
Der wohl problematischste Fall ist die nachträgliche Änderung der Schnittstelle einer zentralen Klasse, von der es zahlreiche Spezialisierungen gibt, beispielsweise im Zusammenhang mit der Umstellung auf eine neue Version einer Klassenbibliothek. Hierbei sind vor allem zwei Fälle zu unterscheiden:[34]
Anpassung der Signatur einer bestehenden virtuellen Methode oder deren Umbenennung
Falls im ersten Fall die neue Methode ohne Implementierung eingeführt wird, als Bestandteil einer abstrakten Klasse, müssen alle Spezialisierungen bei Versionsumstieg nun diese Funktionalität bereitstellen. Weit schwerwiegender ist allerdings, wenn in der Vererbungshierarchie in nachgeordneten Klassen bereits eine gleichnamige virtuelle Methode existierte. Dieser Fall kann in den meisten Sprachen nicht vom Compiler aufgedeckt werden. Diese bestehende virtuelle Methode wird nun in einem Kontext aufgerufen, für den sie nicht implementiert wurde. Wird dieses Problem nicht anhand der Bearbeitung der Dokumentation des Versionswechsels beseitigt, führt es zu inkorrektem Systemverhalten und meist zu einem Laufzeitfehler.[34]
Im zweiten Fall muss die Umbenennung oder Signaturanpassung in den spezialisierenden Klassen nachgezogen werden. Erfolgt dies nicht, hängen die bisherigen Implementierungen nun „in der Luft“, das heißt, sie werden an erforderlichen Stellen nicht mehr aufgerufen, stattdessen wird eine in einer Basisklasse existierende Standardfunktionalität verwendet, die eigentlich vorgesehene angepasste Funktionalität kommt nicht mehr zur Ausführung. Auch dieses Problem kann in einigen Konstellationen nicht vom Compiler aufgedeckt werden.[34]
Die Sicherstellung, dass solche Probleme vom Compiler erkannt werden können, erfordert eigentlich eine vergleichsweise geringfügige Ergänzung einer Sprache. Bei C# beispielsweise ist dies durch das Schlüsselwort override abgedeckt. Bei allen Methoden, die eine virtuelle Methode der Basisklasse überschreiben, muss dieses Schlüsselwort angegeben werden. Dass in den meisten Sprachen wie auch C++ oder Java[A 9] eine derartige Unterstützung fehlt, liegt daran, dass dieser Aspekt bei Konzeption der Sprache keine ausreichende Berücksichtigung fand, und die nachträgliche Einführung eines solchen Schlüsselworts aufgrund großer Kompatibilitätsprobleme auf erheblichen Widerstand stößt.[34]
Auch ohne die Änderung einer Klassenschnittstelle kann es bei Umstellung auf eine neue Version einer Basisklasse zu Problemen kommen. Die Entwickler, die eine „zerbrechliche“ Basisklasse ändern, sind in diesem Fall nicht in der Lage, die negativen Konsequenzen vorauszuahnen, die sich für spezialisierte Klassen durch die Änderung ergeben. Die Gründe hierfür sind vielfältig, im Wesentlichen liegt ein Missverständnis zwischen den Entwicklern der Basisklasse und denen der verwendeten Spezialisierungen vor. Dies liegt zumeist daran, dass die Funktionalität der Basisklasse und auch das von den Spezialisierungen erwartete Verhalten nicht ausreichend präzise spezifiziert sind.[37][38]
Eine häufige Ursache des Fragile Base Class Problems ist die zu großzügige Offenlegung von Implementierungsdetails, die zumeist aus praktischen Gründen erfolgt, wobei auch Teile offengelegt werden, die in einer anfänglichen Version noch nicht ausgereift sind. Die Programmiersprachen erleichtern die Umsetzung sinnvoller Einschränkungen der Freiheitsgrade häufig nicht, beispielsweise sind in Java Methoden grundsätzlich virtuell und müssen als final gekennzeichnet werden, wenn kein Überschreiben durch eine ableitende Klasse möglich sein soll.[39]
Vererbung bei prototypenbasierter Programmierung
Der Begriff Vererbung wird auch bei prototypenbasierten Programmierung verwendet. Bei prototypenbasierten Sprachen wird aber nicht zwischen Klasse und instantiiertem Objekt unterschieden. Dementsprechend ist hier mit Vererbung nicht ganz dasselbe gemeint, denn ein durch Cloning erzeugtes neues Objekt „erbt“ nicht nur die Struktur des auch als Parent bezeichneten Originals, sondern auch die Inhalte. Der Mechanismus zur Nutzung der Methoden des Parent durch die Kopie (Child) entspricht eigentlich einer Delegation. Diese ist im Sinne einer Vererbung verwendbar, hat aber mehr Freiheitsgrade, beispielsweise ist bei einigen derartigen Sprachen der Adressat der Delegation – und damit die „Basisklasse“ – zur Laufzeit austauschbar.[40]
Literatur
Iain D. Craig: Object-Oriented Programming Languages: Interpretation. Springer, London 2007, ISBN 1-84628-773-1.
Bernhard Lahres, Gregor Rayman: Praxisbuch Objektorientierung. Von den Grundlagen zur Umsetzung. Galileo Press, Bonn 2006, ISBN 3-89842-624-6.
Klaus Zeppenfeld: Objektorientierte Programmiersprachen. Einführung und Vergleich von Java, C++, C#, Ruby. Spektrum Akademischer Verlag, München 2004, ISBN 3-8274-1449-0.
Ruth Breu: Objektorientierter Softwareentwurf. Integration mit UML. Springer, Heidelberg 2001, ISBN 3-540-41286-7.
↑Die Modellierung dient hier nur zur Veranschaulichung. Beispielsweise wären Attribute wie Antriebsart, Hubraum und ob ein Anhänger mitgeführt wird in einem realitätsnahen System ebenfalls zu berücksichtigen.
↑Die Menge der möglichen Ausprägungen des Subtyps bildet weiterhin eine Teilmenge des Basistyps, wenn lediglich die Attribute des Basistyps betrachtet werden.
↑Eine solche Änderung kann in der Praxis durchaus nachträglich erfolgen und ohne dass der Entwickler der Basisklasse und der abgeleiteten Klasse sich kennen müssen, beispielsweise bei Verwendung einer Klassenbibliothek und der Umstellung auf eine neue Version.
↑Dieser Aspekt ist ein Grund dafür, warum eine derartige Spezialisierung in einigen Anwendungsfällen ungünstig ist. Dieses Beispiel wird häufig zur Veranschaulichung und Diskussion dieses in Verbindung mit der Vererbung stehenden Problems verwendet und ist auch unter der Bezeichnung Kreis-Ellipse-Problem(circle ellipse problem) bekannt.
↑In Java gilt dieses Prinzip allerdings nur für einen Teil der möglichen Ausnahmetypen, den sogenannten Checked Exceptions.
↑Bei C++ oder Java ist dagegen der Zugriff auf private Eigenschaften anderer Instanzen derselben Klasse möglich, was typischerweise beim Copy-Konstruktor durch direkten Zugriff auf die Eigenschaften des Quellobjekts ausgenutzt wird.
↑Dies stimmt allerdings nicht für C++, wenn die Zuweisung auf Wertebene erfolgt.
↑In Java wurde die generische Programmierung erst ab Version 1.5 unterstützt, für .NET erst mit dem .Net-Framework 2.0. Zuvor basierte die Implementierung der Containerklassen ausschließlich auf diesem Prinzip.
↑In Java Version 1.5 wurde eine Annotation@Override eingeführt, die das Problem aber nur teilweise löst, vor allem da man sie nicht benutzen muss.
Einzelnachweise
↑Vgl. George A. Miller: Wörter. Streifzüge durch die Psycholinguistik. Herausgegeben und aus dem Amerikanischen übersetzt von Joachim Grabowski und Christiane Fellbaum. Spektrum der Wissenschaft, Heidelberg 1993; Lizenzausgabe: Zweitausendeins, Frankfurt am Main 1995; 2. Auflage ebenda 1996, ISBN 3-86150-115-5, S. 210–217.
↑Ole-Johan Dahl, Kristen Nygaard: Class and Subclass Declarations. In: J. N. Buxton (Hrsg.): Simulation Programming Languages. Proceedings of the IFIP working conference on simulation programming languages, Oslo, Mai 1967 North-Holland, Amsterdam, 1968, S. 158–174 (online (Memento vom 10. Juni 2007 im Internet Archive); PDF; 693 kB)
↑ abcPeter H. Fröhlich: Inheritance Decomposed. Inheritance Workshop, European Conference on Object-Oriented Programming (ECOOP), Málaga, 11. Juni 2002.
↑ abcdeBertrand Meyer: The many faces of inheritance: A taxonomy of taxonomy. In: IEEE Computer. Vol. 29, 1996, S. 105–108.
↑Donald Firesmith: Inheritance Guidelines. In: Journal of Object-Oriented Programming. 8(2), 1995, S. 67–72.
↑Ruth Breu: Objektorientierter Softwareentwurf. S. 198 f., siehe Literatur
↑ abcdBernhard Lahres, Gregor Rayman: Praxisbuch Objektorientierung. S. 153–189, siehe Literatur
↑Christoph Schmitz: Spezifikation objektorientierter Systeme Universität Tübingen, 1999, S. 9–12.
↑Eine Ausnahme ist Sather, siehe Iain D. Craig: Object-Oriented Programming Languages: Interpretation. S. 187, siehe Literatur
↑ abIain D. Craig: Object-Oriented Programming Languages: Interpretation. S. 185–190, siehe Literatur
↑Alan Snyder: Inheritance and the development of encapsulated software systems. In: Research Directions in Object-Oriented Programming. Cambridge.1987, S. 165–188.
↑ abIain D. Craig: Object-Oriented Programming Languages: Interpretation. S. 91–98, siehe Literatur
↑ abIain D. Craig: Object-Oriented Programming Languages: Interpretation. S. 98–124, siehe Literatur
↑ abBjarne Stroustrup: Design und Entwicklung von C++. Addison-Wesley, Bonn 1994, ISBN 3-89319-755-9, S. 327–352.
↑Bertrand Meyer: Objektorientierte Softwareentwicklung. S. 296–300, siehe Literatur
↑
Florian Adamsky: Vererbung und Polymorphie. In: Technische Hochschule Mittelhessen, Softwareentwicklung im SS 2014. 2014 (adamsky.it [PDF]).
↑
Kuruvada, Praveen and Asamoah, Daniel and Dalal, Nikunj and Kak, Subhash: The Use of Rapid Digital Game Creation to Learn Computational Thinking. 2010, arxiv:1011.4093.
↑
Manoj Kumar Sah and Vishal Garg: Survey on Types of Inheritance Using Object Oriented Programming with C++. In: International Journal of Computer Techniques. Band1, 2014 (ijctjournal.org [PDF]).
↑Iain D. Craig: Object-Oriented Programming Languages: Interpretation. S. 174–179, siehe Literatur
↑ abcdeErhard Plödereder: OOP-Sprachkonstrukte im Kontext der Softwarewartung. Vortrag bei der Fachtagung Industrielle Software-Produktion, Stuttgart 2001.
↑Victor R. Basili, Lionel Briand, Walcélio L. Melo: A Validation of Object-Oriented Design Metrics as Quality Indicators. In: University of Maryland, Department of Computer Science (Hrsg.): IEEE Transactions on Software Engineering. Band22, Nr.10, Oktober 1996, S.751–761 (englisch).