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.[1] Neben diesem konstruktiven Aspekt dient der 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.[2]
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 konzeptionelle Phase der Softwareentwicklung beschränkter Begriff ist.
In der Programmiersprache Simula wurde 1967 die Vererbung mit weiteren Konzepten Objektorientierter Programmierung – wie Klasse, Objekt und Polymorphie – erstmals eingeführt.[3] Da Klasse sowohl den Datentyp als auch die Funktionalität spezifizieren, wird bei der Vererbung zwischen Klassen sowohl der Typ – spezifiziert durch seine Schnittstelle (engl. Interface) – als auch die Implementierung vererbt. 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:[6]
- 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)
Außer bei der letzten Variante stehen abgeleitete Klasse und Basisklasse idealerweise einer „ist-ein“-Beziehung zueinander.
Ein weiterer orthogonal zu obiger Unterscheidung stehender Aspekt ist die Mehrfachvererbung. Hierbei geht es darum, ob eine abgeleitete Klasse von mehr als einer Basisklasse erben kann. Die Unterstützung der Programmiersprachen ist dabei unterschiedlich, beispielsweise kann in Java und C# eine Klasse zwar von beliebig vielen Schnittstellen, aber nur von einer Klasse erben. C++ und Eiffel bieten dagegen eine uneingeschränktere Form der Mehrfachvererbung.[7]
Varianten der Vererbung von Typ und Impementierung
Implementierungsvererbung
Hierbei wird von der Basisklasse die Implementierung und implizit auch deren Schnittstelle geerbt. Die klassifizierende Verwendung dieser Vererbung ist die anschaulichste Form. In der realen Welt finden sich Beispiele: Amsel als Spezialisierung von Vogel, Notebook als Spezialisierung von Computer oder Quadrat als Spezialisierung von Rechteck. 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. Bei einer Modellierung von Computer und Notebook könnte beispielsweise die Kapazität des Akkus eine solche zusätzliche Eigenschaft sein.
Im folgenden wird in der Programmiersprache Java ein Beispiel für die Ableitung von Quadrat
als Spezialisierung von Rechteck
skizziert. 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, zum anderen 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.[8] Ein wesentliches Produkt dieser Bestrebungen war der abstrakte Datentyp, der 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[9] 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.[10]
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.[6] 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[11], 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 nun eine Anwendung, die meint ein Rechteck vor sich zu haben, die Länge des Rechtecks verdoppelt und dabei eine Verdopplung der Fläche erwartet, ist die damit verbundene Vervierfachung der Fläche überraschend, die auftritt, wenn es sich bei dem Objekt um eine Instanz des Typs Quadrat
handelt.[12]
Im Zusammenhang mit dem liskovsche Substitutionsprinzip steht auch die Behandlung der Varianz bei den Signaturen überschriebener Methoden. Viele Programmiersprachen unterstützen hier keine Varianz, dass 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, dass heißt, der Rückgabewert darf spezieller sein als bei der Basisklasse, alle anderen Argumente können allgemeiner sein.[13]
Die fehlende Trennung zwischen Typ- und Implementierungsvererbung führt in der Praxis häufig dazu, dass im Klasseninterface Implementierungsdetails durchscheinen.[14] 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 auf in Verbindung mit verteilten Architekturen wie CORBA oder COM notwendig.[10]
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 Berechnung der 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.[6]
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:[15]
- Ö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.[15]
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 Beispiel für Mehrfachvererbung liefert eine Klasse zur Abbildung einer Combo-Box, die ein typischer Bestandteil von Klassenbibliotheken zur Implementierung einer Grafischen Benutzeroberfläche ist. Eine Combo-Box vereinigt dabei die Eigenschaften einer Drop-Down-Liste und eines Eingabefelds . Es ist dabei eine nahe liegender Entwurf, dass eine Klasse, die eine Combo-Box repräsentiert, sowohl von der die Drop-Down-Liste als auch das Eingabefeld abbildenden Klasse erbt.[16]
Die Notwendigkeit von Mehrfachvererbung ist umstritten, es wird nicht von allen Sprachen unterstützt. Eine umfassende Unterstützung bietet beispielsweise 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.[17]
Beispiel

Ein Fahrzeug besitzt bestimmte Attribute, diese können z.B. Höchstgeschwindigkeit, maximale Zuladung oder auch Farbe sein. Die Klasse Kraftfahrzeug erbt all diese Attribute, kann aber noch zusätzliche Attribute besitzen, z.B. Leistung oder Drehmoment. Des Weiteren kann ein Kraftfahrzeug auch zusätzliche Methoden wie Beschleunigen besitzen, welche die Basisklasse Fahrzeug nicht kennt.
Die Klasse Personenkraftwagen kann dann wiederum von Kraftfahrzeug abgeleitet werden und weitere zusätzliche Attribute wie Anzahl der Sitze besitzen. Durch die Ableitung von Kraftfahrzeug erbt der Personenkraftwagen automatisch alle Attribute von Fahrzeug (mehrstufige Vererbung).
Ob eine Klasse in einer Vererbungsbeziehung zu einer anderen Klasse steht, lässt sich durch eine einfache Ist-Ein-Regel feststellen. Sobald die Aussage „(Objekt der abgeleiteten Klasse) ist ein (Objekt der Basisklasse)“ zutrifft, stehen beide Klassen in einer Vererbungsbeziehung.
- Ein Personenkraftwagen ist ein Kraftfahrzeug,
- Ein Personenkraftwagen ist ein Fahrzeug, aber
- Ein Fahrzeug ist kein Personenkraftwagen. (i.e.: "nicht jedes Fahrzeug ist ein Personenkraftwagen")
- Ein Personenkraftwagen ist kein Sitz
- Ein Personenkraftwagen hat einen Sitz.
Hat ein bezeichnet also Attribute einer Klasse.
Vererbung bei prototypenbasierter Programmierung
Bei der prototypenbasierten Programmierung kann nicht zwischen Schnittstellen- und Implementierungsvererbung unterschieden werden, da noch nicht einmal zwischen Klasse und Objekt unterschieden wird. Auch sind bei der Vererbung die weiteren Gestaltungs- und Modellierungsmöglichkeiten gegenüber wirklich objektorientierten Sprachen stark eingeschränkt.
Siehe auch
Literatur
- 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
- Bernhard Lahres , Gregor Rayman: Praxisbuch Objektorientierung. Von den Grundlagen zur Umsetzung. Galileo Press, Bonn 2006, ISBN 3-89842-624-6
- Iain D. Craig: Object-Oriented Programming Languages: Interpretation. Springer Verlag, London 2007, ISBN 1-84628-773-1
Einzelnachweise
- ↑ B. Meyer: Objektorientierte Softwareentwicklung. Seite 67, siehe Literatur
- ↑ R. Breu: Objektorientierter Softwareentwurf. Seite 38ff, siehe Literatur
- ↑ B. Meyer: Objektorientierte Softwareentwicklung. Seite 451–464, siehe Literatur
- ↑ a b Peter H. Fröhlich: Inheritance Decomposed. Inheritance Workshop, European Conference on Object-Oriented Programming (ECOOP), Malaga, 11. Juni 2002.
- ↑ a b B. Meyer: The many faces of inheritance: A taxonomy of taxonomy. In: IEEE Computer, Vol. 29, Seite 105–108, 1996
- ↑ a b c Lahres , Rayman: Praxisbuch Objektorientierung. Seite 153–189, siehe Literatur
- ↑ Lahres , Rayman: Praxisbuch Objektorientierung. Seite 260–287, 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.
- ↑ I. Craig: Object-Oriented Programming Languages: Interpretation. Seite 174–179, siehe Literatur
- ↑ Alan Snyder: Inheritance and the development of encapsulated software systems. In: Research Directions in Object-Oriented Programming. Seite 165–188, Cambridge.1987
- ↑ a b I. Craig: Object-Oriented Programming Languages: Interpretation. Seite 25–31, siehe Literatur
- ↑ Obwohl dies ein nahe liegender Entwurf ist, wird dies aus technischen Gründen bei den meisten Klassenbibliotheken nicht so implementiert (siehe beispielsweise.NET Framework Class Library), Gründe hierzu siehe auch Mehrfachvererbung
- ↑ I. Craig: Object-Oriented Programming Languages: Interpretation. Seite 98–124, siehe Literatur