Vererbung (Programmierung)

Die Vererbung (engl. Inheritance) ist eines der grundlegenden Konzepte der Objektorientierung. Sie dient dazu, aufbauend auf existierenden Klassen neue zu schaffen. Diese können 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 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 Gegenstand des Erbens nennt man meist Ableitung oder Spezialisierung, die Umkehrung hiervon Generalisierung, was ein vorwiegend auf die Modellebene beschränkter Begriff ist. In UML wird eine Vererbungsbeziehung durch einen Pfeil mit einer dreieckigen Spitze dargestellt, der von der abgeleiteten Klasse zur Basisklasse zeigt. Geerbte Eigenschaften und Methoden werden in der Darstellung der abgeleiteten Klasse nicht wiederholt.
In der Programmiersprache Simula wurde 1967 die Vererbung mit weiteren Konzepten objektorientierter Programmierung erstmals eingeführt.[1] Letztere hat seitdem in der Softwareentwicklung wichtige neue Perspektiven eröffnet und auch die komponentenbasierte Entwicklung ermöglicht.
Abgeleitete Klasse und Basisklasse stehen typischerweise in einer „ist-ein“-Beziehung zueinander. Da Klassen der Spezifikation von Datentyp und Funktionalität dienen, kann bei der Vererbung sowohl der Typ – spezifiziert durch seine Schnittstelle (engl. Interface) – als auch die Implementierung vererbt werden. Einige Programmiersprachen trennen zumindest teilweise zwischen diesen Aspekten. 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.
Beispiel

Das folgende Beispiel stellt einen möglichen Ausschnitt aus dem Anwendungsgebiet der Unterstützung eines Fahrzeugverleihs dar. Eine Basisklasse Fahrzeug
enthält die Eigenschaften Leergewicht
und ZulässigesGesamtgewicht
. Die Spezialisierungen Kraftfahrzeug
und Fahrrad
ergänzen weitere Eigenschaften zu den von der Basisklasse geerbten Gewichtsangaben. In jedem Objekt der Klasse Kraftfahrzeug
werden also die Eigenschaften Leergewicht
, ZulässigesGesamtgewicht
, Höchstgeschwindigkeit
und Leistung
gespeichert.
In der Klasse Kraftfahrzeug
wird eine Methode Prü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.[2] 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 Eigenschaften 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.

PKW
Innerhalb eines dieses Modell implementierenden Anwendungsprogramms würde zur Prüfung, ob eine Fahrerlaubnis gültig ist, nach Eingabe der entsprechenden Daten das konkrete zu mietende Fahrzeug instantiiert, das heißt, die entsprechende Spezialisierung.
Zudem würde ebenfalls ein Objekt für die vorliegende Fahrererlaubnis erzeugt. Dieses würde der Methode PrüfeFahrerlaubnis
des konkreten Fahrzeug-Objekts übergeben und die Rückgabe ausgewertet und beispielsweise dem Sachbearbeiter angezeigt. 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 eine spezielle Tabelle, die der Ermittlung des Einsprungspunkts der passenden konkreten Implementierung bei einem Methodenaufruf dient.[3]
Anwendungsfälle der Vererbung
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 nicht der Softwarequalität eher abträglich ist.[4][5]
Folgende Anwendungsfälle werden empfohlen oder tauchen in der Praxis auf:[5][6]
- Subtyp-Vererbung: Bei dieser 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 stellen 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 oder Funktionalität.[7] Diese Variante beinhaltet auch funktionale Anpassungen durch Überschreiben von Methoden, um Funktionalität zu ergänzen, die in der Basisklasse nicht relevant ist. Auch schließt diese Variante den Fall ein, dass nur eine Teil der Funktionalität einer abstrakten Klasse in der abgeleiteten – ebenfalls abstrakten – Klasse implementiert wird, und die zusätzlich erforderlichen 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[8]und Comparable[9]. 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).
Daneben tauchen in der Praxis Fälle auf, die eindeutig nicht als sinnvolle Verwendung der Vererbung anzusehen sind. Insbesondere bei 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 Weibliche Person
und Männliche Person
in den wenigsten Fällen zweckmäßig sein und bei der Modellierung der eigentlich relevanten Aspekte eher behindern. Eine weitere fehlerhafte 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 eine bildhaftes Beispiel dieses Modellierungsfehlers dar.[5]
![]() |
![]() |
Beim Übergang von der objektorientierten Modellierung zur Programmierung gibt es die Situation, dass die Modellierung einer klassifizierenden Hierarchie der fachlichen Anwendungsobjekte nicht so 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 nicht so ohne weiteres von der Rolle des Mitarbeiter zur Rolle des Kunden wechseln. Zum anderen kann eine Person auf diese Weise auch nicht mehrere Rollen gleichzeitig spielen. 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 assoziative Beziehung zwischen Person
und ihren Rollen.[10]
Varianten der Vererbung von Typ und Implementierung
Bei der Vererbung kann sowohl der Typ – spezifiziert durch seine Schnittstelle – als auch die Funktionalität vererbt werden. Die Konsequenzen dieser Doppelfunktion der Vererbung werden seit Jahren kontrovers diskutiert.[4][5] Auch wenn neuere Programmiersprachen wie Java und C# keine durchgängige Trennung dieser Vererbungsvarianten unterstützen, bieten diese für Schnittstellen (interface) und Klassen (class) zwei formal getrennte Konzepte an. Es lassen sich drei Fälle unterscheiden:[11]
- 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 C# 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.
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 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 Funktionen zur Berechnung des Umfangs und der Länge der Diagonalen des Rechtecks. Die Spezialisierung Quadrat
erbt diese Funktionalität (Schlüsselwort extends
). Der Konstruktor für Quadrat
nimmt nur noch einen statt zwei Parameter, da Länge und Breite ja übereinstimmen müssen. Die in der Klasse Rechteck
implementierten Berechungen 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.
public class Rechteck
{
private double laenge;
private double breite;
public Rechteck(double laenge, double breite)
{
this.breite = breite;
this.laenge = laenge;
}
public double getLaenge() { return laenge; }
public double getBreite() { return breite; }
public double getUmfang()
{
return 2 * laenge + 2 * breite;
}
public double getDiagonale()
{
return java.lang.Math.sqrt(laenge * laenge +
breite * breite);
}
}
|
public class Quadrat extends Rechteck
{
static double wurzel2 = java.lang.Math.sqrt(2);
// Einmalige Berechnung der Wurzel aus 2
public Quadrat(int laenge)
{
super(laenge, laenge);
// Aufruf des Konstruktors der Basisklasse
}
public double getDiagonale()
{
return wurzel2 * 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 von letzteren ist, dass solche Spezifikationen mit einer mathematischen Semantik versehen werden können.[12] Ein wesentliches Produkt 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[13] eine durchgängige Trennung der Vererbung von Schnittstelle und Implementierung explizit unterstützt wird. Relativ neue Sprachen wie Java und C# 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 mal 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ändige 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.[14]
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 Subtyp 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.[11] Wenn etwa beim oben skizzierten Beispiel in der Basisklasse Recheck
zur nachträglichen Veränderung der Größe die Funktionen setLaenge
und setBreite
eingeführt werden[15], 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.[16]
Die fehlende Trennung zwischen Typ- und Implementierungsvererbung führt in der Praxis häufig dazu, dass im Klasseninterface Implementierungsdetails durchscheinen.[17] Eine Strategie zur Vermeidung dieses Effekts ist die Verwendung abstrakter Klassen oder Schnittstellen an 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.[14]
Reine Implementierungsvererbung
Bei der reinen Implementierungsvererbung nutzt die erbende Klasse die Funktionalität 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 C# gibt es sie nicht. Eine Alternative bei letzteren Sprachen ist die Verwendung von Delegation, die etwas mehr Programmcode erfordert.[11]
Kovarianz und Kontravarianz
→ Hauptartikel: Kovarianz und Kontravarianz
Im Zusammenhang mit dem liskovschen Substitutionsprinzip steht auch die Behandlung der Varianz bei den Signaturen überschriebener Methoden. Viele Programmiersprachen unterstützen hier keine Varianz, das heißt, die Typen der Argumente und der Rückgabetyp überschriebener Methoden müssen exakt übereinstimmen. Dem liskovschen Prinzip entsprechend ist dabei die Unterstützung von Kovarianz für den Rückgabewert einer Funktion und die Unterstützung von Kontravarianz für alle anderen Argumente, das heißt, der Rückgabewert darf spezieller sein als bei der Basisklasse, alle anderen Argumente können allgemeiner sein.[18]
Von wenigen Sprachen wird die Deklaration der Ausnahmen (engl. 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.[19][20]
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.[21]
Datenkapselung im Rahmen der Vererbung
Bei der Spezifizierung der Sichtbarkeit von Eigenschaften von Klassen (Datenkapselung) wird häufig unterschieden, ob der Zugriff durch eine ableitende Klasse oder „von außen“, das heißt, bei einer anderweitigen Verwendung der Klasse, erfolgt. Bei den meisten Sprachen werden drei Fälle unterschieden:[22]
- Öffentlich (public): Die Eigenschaft ist ohne Einschränkungen sichtbar
- Geschützt (protected): Die Eigenschaft ist für ableitende 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 C# führen zusätzlich noch Varianten ein, die die Sichtbarkeit auf sprachspezifische Untereinheiten der Programmstruktur (Assembly oder Package) begrenzen.
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 C# dagegen ist keine solche Änderung der Sichtbarkeit bei Vererbung möglich.[22]
Mehrfachvererbung
→ Hauptartikel: Mehrfachvererbung

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.[23]

Ein anderes Beispiel für Mehrfachvererbung ergibt sich durch die Erweiterung des einführenden Beispiels um die Klassen Schienenfahrzeug
und Zweiwegefahrzeug
. Letztere erbt dabei von sowohl von sowohl von Kraftfahrzeug
als auch von Schienenfahrzeug
und hat somit sowohl alle Eigenschaften der Kraftfahrzeuge, als auch die zusätzliche Eigenschaft Spurweite
, die 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++ und Eiffel. Java und C# 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.[23]
Neben einem erheblichen zusätzlichen Implementierungaufwand für Compiler und Laufzeitumgebung gibt es vor allem zwei Gründe für die häufige fehlende oder eingeschränkte Unterstützung:[24]
- Mögliche Namenskollisionen bei geerbten Eigenschaften 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.[24] Eiffel bietet auch beide Möglichkeiten und dies sogar auf Ebene der Attribute.[25] 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.
Zentrale Wurzel des Klassenbaums
Viele objektorientierte Programmiersprachen verfügen über eine zentrale Klasse, von der alle Klassen – über wie viele Stufen auch immer – letztlich abgeleitet sind. Zu den wenigen Ausnahmen zählen C++ oder Python. Dagegen gibt es beispielsweise in Java und C# eine solche Klasse, diese heißt Object
, in Eiffel wird sie mit ANY
bezeichnet. In diesen Sprachen 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[26], hat allerdings den Nachteil, dass eine Typprüfung erst zur Laufzeit möglich ist.[27]
Vererbung im Kontext der Softwareentwicklung und -wartung
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.[28]
Neben diesen 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.[29]
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:[28]
- Hinzufügen einer neuen virtuellen Methode
- 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 Funktion 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.[28]
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, statt dessen wird eine in einer Basisklasse existierende Standardfunktionalität verwendet, die eigentlich vorgesehene angepasste Funktionalität kommt nicht mehr zur Ausführung. Auch diesen Problem kann in einigen Konstellationen nicht vom Compiler aufgedeckt werden.[28]
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 overwrite
abgedeckt. Bei allen Funktionen, die eine virtuelle Methode der Basisklasse überschrieben, muss dieses Schlüsselwort angegeben werden. Dass in den meisten Sprachen wie auch C++ oder Java[30] 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.[28]
Fragile Base Class Problem
→ Hauptartikel: Fragile Base Class Problem
Auch ohne die Änderung einer Klassenschnittstelle kann es bei Umstellung auf eine neue Version einer Basisklasse zu Problemen kommen. Der Entwickler der „zerbrechlichen“ Basisklasse ist dabei bei einer Änderung nicht in der Lage, die negativen Konsequenzen vorauszuahnen, die sich für spezialisierende Klassen hieraus ergeben. Die Gründe hierfür sind vielfältig, im Wesentlichen liegt ein Missverständnis zwischen den Entwicklern der Basisklasse und denen der verwendende 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 ist.[31][32]
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 offen gelegt 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 Überschrieben durch eine ableitende Klasse möglich sein soll.[33]
Vererbung bei prototypenbasierter Programmierung
Der Begriff Vererbung wird auch bei prototypenbasierten Programmierung verwendet. Bei prototypenbasierten Sprachen wird aber nicht zwischen Klasse und Objekt unterschieden. Dementsprechend ist hier mit Vererbung nicht ganz dasselbe gemeint. Auf der einen Seite „erbt“ ein durch Cloning erzeugtes neues Objekt 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“ – bei Laufzeit austauschbar.[34]
Siehe auch
Literatur
- Iain D. Craig: Object-Oriented Programming Languages: Interpretation. Springer Verlag, 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
- Bertrand Meyer: Objektorientierte Softwareentwicklung. Hanser Verlag, München 1990, ISBN 3-446-15773-5
- Ruth Breu: Objektorientierter Softwareentwurf. Integration mit UML. Springer Verlag, Heidelberg 2001, ISBN 3-540-41286-7
- Grady Booch, James Rumbaugh, Ivar Jacobson: Das UML-Benutzerhandbuch. Addison-Wesley, Bonn 1999, ISBN 3-8273-1486-0
Einzelnachweise
- ↑ 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, Seite 158–174 (online)
- ↑ Die Modellierung dient hier nur zur Veranschaulichung. Beispielsweise wären Eigenschaften wie Antriebsart, Hubraum und ob ein Anhänger mitgeführt wird in einem realitätsnahen System ebenfalls zu berücksichtigen.
- ↑ Bjarne Stroustrup: Design und Entwicklung von C++. Seite 90–98, Addison-Wesley, Bonn 1994, ISBN 3-89319-755-9
- ↑ a b c Peter H. Fröhlich: Inheritance Decomposed. Inheritance Workshop, European Conference on Object-Oriented Programming (ECOOP), Malaga, 11. Juni 2002.
- ↑ a b c d e B. Meyer: The many faces of inheritance: A taxonomy of taxonomy. In: IEEE Computer, Vol. 29, Seite 105–108, 1996
- ↑ Donald Firesmith: Inheritance Guidelines. In: Journal of Object-Oriented Programming. 8(2), 1995, Seite 67–72
- ↑ Die Menge der möglichen Ausprägungen des Subtyps bildet weiterhin eine Teilmenge des Basistyps, wenn lediglich die Attribute des Basistyps berücksichtigt werden.
- ↑ siehe beispielsweise: MSDN, .NET Framework-Klassenbibliothek: ISerializable-Schnittstelle
- ↑ siehe beispielsweise: Java 2 Platform, Standard Edition, v 1.4.2, API Specification: Interface Comparable
- ↑ R. Breu: Objektorientierter Softwareentwurf. Seite 198f, siehe Literatur
- ↑ a b c Lahres , Rayman: Praxisbuch Objektorientierung. Seite 153–189, siehe Literatur
- ↑ C. Schmitz: Spezifikation objektorinierter Systeme. Seite 9–12, Universität Tübingen, 1999
- ↑ Eine Ausnahme ist Sather, siehe I. Craig: Object-Oriented Programming Languages: Interpretation. Seite 187, siehe Literatur
- ↑ a b I. Craig: Object-Oriented Programming Languages: Interpretation. Seite 185–190, siehe Literatur
- ↑ 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, von der auf eine neue Version umgestellt wird
- ↑ Nicht nur dieser Aspekt ist ein Grund dafür, warum eine derartige Spezialisierung eigentlich ungünstig ist und hier nur zur Veranschaulichung dient, dieses Problem ist bekannt und wird meist unter der Bezeichnung Kreis-Ellipse-Problem (circle ellipse proplem) diskutiert.
- ↑ Alan Snyder: Inheritance and the development of encapsulated software systems. In: Research Directions in Object-Oriented Programming. Seite 165–188, Cambridge.1987
- ↑ I. Craig: Object-Oriented Programming Languages: Interpretation. Seite 174–179, siehe Literatur
- ↑ Anna Mikhailova, Alexander Romanovsky: Supporting Evolution of Interface Exceptions. In: Advances in exception handling techniques. Seite 94–110, Springer-Verlag, New York 2001, ISBN 3-540-41952-7
- ↑ In Java gilt dieses Prinzip allerdings nur für einen Teil der möglichen Ausnahmetypen, den sogenannten Checked Exceptions
- ↑ B. Meyer: Objektorientierte Softwareentwicklung. Seite 275–278, siehe Literatur
- ↑ a b I. Craig: Object-Oriented Programming Languages: Interpretation. Seite 25–31, siehe Literatur
- ↑ a b I. Craig: Object-Oriented Programming Languages: Interpretation. Seite 98–124, siehe Literatur
- ↑ a b Bjarne Stroustrup: Design und Entwicklung von C++. Seite 327–352, Addison-Wesley, Bonn 1994, ISBN 3-89319-755-9
- ↑ B. Meyer: Objektorientierte Softwareentwicklung. Seite 296–300, siehe Literatur
- ↑ In Java wurde die generische Programmierung erst ab Version 1.5 unterstützt, in C# erst mit Version 2.0. Zuvor basierte die Implementierung der Containerklassen ausschließlich auf diesem Prinzip.
- ↑ I. Craig: Object-Oriented Programming Languages: Interpretation. Seite 173f, siehe Literatur
- ↑ a b c d e Erhard Plödereder: OOP-Spraschkonstrukte im Kontext der Softwarewartung. Vortrag bei der Fachtagung Industrielle Software-Produktion, Stuttgart 2001
- ↑ Jeff Offut, Roger Alexander: A Fault Model for Subtype Inheritance and Polymorpism. In: The Twelfth IEEE International Symposium on Software Reliability Engineering, Seite 84–95, Hong Kong 2001
- ↑ In Java Version 1.5 wurde eine Annotation
@Overwrite
eingeführt, die das Problem aber nur teilweise löst, vor allem da man sie nicht benutzen muss - ↑ Lahres , Rayman: Praxisbuch Objektorientierung. Seite 238–257, siehe Literatur
- ↑ Leonid Mikhajlov, Emil Sekerinski: A Study of The Fragile Base Class Problem. In: Proceedings of the 12th European Conference on Object-Oriented Programming, Seite 355–382, 1998, ISBN 3-540-64737-6
- ↑ Joshua Bloch: Effective Java. Seite 87–92, Addison-Wesley, 2008, ISBN 0-321-35668-3
- ↑ I. Craig: Object-Oriented Programming Languages: Interpretation. Seite 57–72, siehe Literatur
Weblinks
- Andreas Siegrist (Universität Zürich): Ein Vergleich der Sprachen Smalltalk, Eiffel und C# bezüglich der Vererbung, Mehrfachvererbung, Polymorphismus und der Dynamischen Bindung
- Axel Schmolitzky (Universität Hamburg): Konzepte und Mechanismen: Vererbung, das Goto der 90er
- University of Cyprus: A Critical Look at Inheritance.