Wann wird Vererbung verwendet?

Aktualisiert: November 2007

Vererbung ist ein sinnvolles Programmierungskonzept; es kann jedoch schnell falsch verwendet werden. Oft eignen sich Schnittstellen besser. In diesem Thema und Verwendungsmöglichkeiten für Schnittstellen wird erläutert, wann die verschiedenen Herangehensweisen verwendet werden sollten.

Vererbung ist unter folgenden Umständen sinnvoll:

  • Die Vererbungshierarchie stellt eine "ist-Beziehung" und keine "hat-Beziehung" dar.

  • Sie können Code aus Basisklassen wiederverwenden.

  • Sie müssen die gleichen Klassen und Methoden auf unterschiedliche Datentypen anwenden.

  • Die Klassenhierarchie ist verhältnismäßig flach, und es ist unwahrscheinlich, dass andere Entwickler viele weitere Ebenen einfügen.

  • Sie möchten globale Änderungen an abgeleiteten Klassen vornehmen, indem Sie eine Basisklasse ändern.

Diese Fälle werden nachfolgend beschrieben.

Vererbung und "Is a"-Beziehungen

Es gibt zwei Möglichkeiten, Klassenbeziehungen in objektorientiertem Programmieren zu zeigen: die "is a"- und die "has a"-Beziehung. In einer "is a"-Beziehung ist die abgeleitete Klasse deutlich erkennbar eine Art der Basisklasse. Beispielsweise stellt eine Klasse mit dem Namen PremierCustomer (Hauptkunde) eine "is a"-Beziehung mit einer Basisklasse mit dem Namen Customer (Kunde) dar, da der Hauptkunde ein Kunde ist. Eine Klasse mit dem Namen CustomerReferral (Kundenverweis) stellt eine "has a"-Beziehung mit der Customer-Klasse (Kunde) dar, da ein Kundenverweis sich auf einen Kunden bezieht (einen Kunden "hat"), aber keine Art von Kunde ist.

Objekte in einer Vererbungshierarchie müssen eine "is a"-Beziehung mit ihrer Basisklasse haben, da sie die in der Basisklasse definierten Felder, Eigenschaften, Methoden und Ereignisse erben. Klassen, die eine "has a"-Beziehung mit anderen Klassen darstellen, eignen sich nicht für Vererbungshierarchien, da sie möglicherweise ungeeignete Eigenschaften und Methoden erben. Wenn z. B. die Klasse CustomerReferral von der Klasse Customer abgeleitet wurde, könnte sie sinnlose Eigenschaften wie z. B. ShippingPrefs und LastOrderPlaced erben. "Has a"-Beziehungen wie diese sollten mithilfe von nicht verwandten Klassen oder Schnittstellen dargestellt werden. Die folgende Abbildung zeigt ein Beispiel von "is a"- und "has a"-Beziehungen.

Basisklassen und Wiederverwendung von Code

Ein weiterer Grund für die Verwendung von Vererbung ist die Wiederverwendbarkeit des Codes. Gut entworfene Klassen können einmal gedebuggt und immer wieder als Basis für neue Klassen verwendet werden.

Ein gängiges Beispiel für die effektive Wiederverwendung von Code sind Bibliotheken, die Datenstrukturen verwalten. Angenommen, Sie haben eine große Geschäftsanwendung, die mehrere Arten von Listen im Speicher verwaltet. Eine ist eine Kopie Ihrer Kundendatenbank im Speicher, die am Anfang der Sitzung aus Schnelligkeitsgründen aus einer Datenbank eingelesen wird. Die Datenstruktur könnte folgendermaßen aussehen:

Class CustomerInfo
    Protected PreviousCustomer As CustomerInfo
    Protected NextCustomer As CustomerInfo
    Public ID As Integer
    Public FullName As String

    Public Sub InsertCustomer(ByVal FullName As String)
        ' Insert code to add a CustomerInfo item to the list.
    End Sub

    Public Sub DeleteCustomer()
        ' Insert code to remove a CustomerInfo item from the list.
    End Sub

    Public Function GetNextCustomer() As CustomerInfo
        ' Insert code to get the next CustomerInfo item from the list.
        Return NextCustomer
    End Function

    Public Function GetPrevCustomer() As CustomerInfo
        'Insert code to get the previous CustomerInfo item from the list.
        Return PreviousCustomer
    End Function
End Class

Die Anwendung kann auch eine ähnliche Liste von Produkten enthalten, die der Benutzer einer Einkaufswagenliste hinzugefügt hat, wie im folgenden Codefragment gezeigt:

Class ShoppingCartItem
    Protected PreviousItem As ShoppingCartItem
    Protected NextItem As ShoppingCartItem
    Public ProductCode As Integer
    Public Function GetNextItem() As ShoppingCartItem
        ' Insert code to get the next ShoppingCartItem from the list.
        Return NextItem
    End Function
End Class

Sie können hier ein Muster erkennen: Zwei Listen verhalten sich gleich (Einfügungen, Löschungen und Abrufe), werden jedoch auf verschiedene Datentypen angewendet. Die Verwaltung von zwei CodeBases zur Ausführung derselben Funktionen ist ineffizient. Die effizienteste Lösung ist, die Listenverwaltung in die eigene Klasse auszulagern und anschließend von dieser Klasse auf verschiedene Datentypen zu vererben:

Class ListItem
    Protected PreviousItem As ListItem
    Protected NextItem As ListItem
    Public Function GetNextItem() As ListItem
        ' Insert code to get the next item in the list.
        Return NextItem
    End Function
    Public Sub InsertNextItem()
        ' Insert code to add a item to the list.
    End Sub

    Public Sub DeleteNextItem()
        ' Insert code to remove a item from the list.
    End Sub

    Public Function GetPrevItem() As ListItem
        'Insert code to get the previous item from the list.
        Return PreviousItem
    End Function
End Class

Die ListItem-Klasse muss nur einmal gedebuggt werden. Sie können dann Klassen erstellen, die diese verwenden, ohne erneut über Listenverwaltung nachdenken zu müssen. Beispiel:

Class CustomerInfo
    Inherits ListItem
    Public ID As Integer
    Public FullName As String
End Class
Class ShoppingCartItem
    Inherits ListItem
    Public ProductCode As Integer
End Class

Obwohl die Wiederverwendung von auf Vererbung basierendem Code ein leistungsstarkes Tool ist, birgt es auch Risiken. Selbst perfekt entworfene Systeme ändern sich manchmal in einer Weise, die die Entwickler nicht vorhersehen können. Änderungen an einer vorhandenen Klassenhierarchie können manchmal unerwünschte Folgen haben. Einige Beispiele hierzu finden Sie im Abschnitt "Das Problem mit instabilen Basisklassen" unter Basisklassen-Entwurfsänderung nach der Bereitstellung.

Austauschbare abgeleitete Klassen

Abgeleitete Klassen in einer Klassenhierarchie können manchmal im Wechsel mit ihrer Basisklasse verwendet werden. Dieser Prozess wird auf Vererbung basierender Polymorphismus genannt. Dieser Ansatz kombiniert die besten Funktionen des auf Schnittstellen basierenden Polymorphismus mit der Möglichkeit, den Code von einer Basisklasse wiederzuverwenden oder zu überschreiben.

Dies kann beispielsweise in einem Zeichenpaket sehr nützlich sein. Betrachten Sie z. B. das folgende Codefragment, das keine Vererbung verwendet:

Sub Draw(ByVal Shape As DrawingShape, ByVal X As Integer, _
    ByVal Y As Integer, ByVal Size As Integer)

    Select Case Shape.type
        Case shpCircle
            ' Insert circle drawing code here.
        Case shpLine
            ' Insert line drawing code here.
    End Select
End Sub

Diese Möglichkeit birgt einige Probleme. Wenn später eine Option für Ellipsen hinzugefügt werden soll, muss der Quellcode geändert werden. Es ist jedoch möglich, dass die Zielbenutzer gar keinen Zugriff auf den Quellcode haben. Ein schwierigeres Problem besteht darin, dass beim Zeichnen einer Ellipse ein anderer Parameter erforderlich ist (Ellipsen haben sowohl einen maximalen als auch einen minimalen Durchmesser), der im Fall einer Linie irrelevant ist. Wenn anschließend eine Polylinie (mehrere verbundene Linien) hinzugefügt werden soll, würde ein anderer Parameter hinzugefügt werden, der für die anderen Fälle irrelevant wäre.

Vererbung löst die meisten dieser Probleme. Gut entworfene Basisklassen überlassen den abgeleiteten Klassen die Implementierung von bestimmten Methoden, sodass jede Form verwendet werden kann. Andere Entwickler können Methoden in abgeleitete Klassen implementieren, indem sie die Dokumentation für die Basisklasse verwenden. Andere Klassenelemente (wie z. B. die X- und Y-Koordinaten) können in die Basisklasse integriert werden, da sie von allen abhängigen Klassen verwendet werden. Beispielsweise kann Draw eine MustOverride-Methode sein:

MustInherit Class Shape
    Public X As Integer
    Public Y As Integer
    MustOverride Sub Draw()
End Class

Dann könnten Sie in dieser Klasse die für verschiedene Formen erforderlichen Elemente hinzufügen. Beispielsweise benötigt eine Line-Klasse nur ein Length-Feld:

Class Line
    Inherits Shape
    Public Length As Integer
    Overrides Sub Draw()
        ' Insert code here to implement Draw for this shape.
    End Sub
End Class

Dieser Ansatz ist sinnvoll, da andere Entwickler, die keinen Zugriff auf Ihren Quellcode haben, Ihre Basisklasse mit neuen abgeleiteten Klassen erweitern können, sofern dies erforderlich ist. Beispielsweise kann eine Klasse mit dem Namen Rectangle von der Line-Klasse abgeleitet werden.

Class Rectangle
    Inherits Line
    Public Width As Integer
    Overrides Sub Draw()
        ' Insert code here to implement Draw for the Rectangle shape.
    End Sub
End Class

Dieses Beispiel zeigt, wie Sie von allgemeinen Klassen zu stark spezialisierten Klassen übergehen, indem Sie jeder Stufe Implementierungsdetails hinzufügen.

Zu diesem Zeitpunkt sollten Sie erneut überprüfen, ob die abgeleitete Klasse wirklich eine "is a"-Beziehung darstellt oder ob es sich um eine "has a"-Beziehung handelt. Wenn die neue Rechteckklasse nur aus Linien besteht, ist Vererbung nicht gut geeignet. Wenn es sich bei dem neuen Rechteck jedoch um eine Linie mit einer Breiteneigenschaft handelt, ist die "is a"-Beziehung gewährleistet.

Flache Klassenhierarchien

Vererbung eignet sich am besten für relativ flache Klassenhierarchien. Die Entwicklung von sehr komplexen Klassenhierarchien kann schwierig sein. Bei der Frage, ob eine Klassenhierarchie verwendet werden soll, müssen Sie ihre Vorteile gegen die Nachteile der Komplexität abwägen. Allgemein gilt, dass die Hierarchien auf sechs Ebenen oder weniger beschränkt sein sollten. Die maximale Verzweigung für eine bestimmte Klassenhierarchie hängt jedoch von einer Vielzahl von Faktoren ab, einschließlich des Grades der Komplexität auf jeder Ebene.

Globale Änderungen an abgeleiteten Klassen durch die Basisklasse

Eine der leistungsstärksten Funktionen der Vererbung ist die Fähigkeit, Änderungen in einer Basisklasse vorzunehmen, die an abgeleitete Klassen weitergegeben werden. Wenn Sie diese Funktion sorgfältig verwenden, können Sie die Implementierung einer einzigen Methode so aktualisieren, dass Dutzende oder sogar Hunderte von abgeleiteten Klassen diesen neuen Code verwenden können. Dies ist jedoch nicht ganz ungefährlich, da solche Änderungen Probleme bei geerbten Klassen verursachen können, die von anderen Entwicklern entworfen wurden. Sie müssen sorgfältig vorgehen, um sicherzustellen, dass die neue Basisklasse mit den Klassen, die das Original verwenden, kompatibel ist. Vermeiden Sie insbesondere das Ändern des Namens oder des Typs von Basisklassenmembern.

Angenommen, Sie entwerfen eine Basisklasse mit einem Feld des Typs Integer, um PLZ-Codeinformationen zu speichern, und andere Entwickler haben abgeleitete Klassen erstellt, die das geerbte PLZ-Codefeld verwenden. Nehmen Sie weiter an, dass das PLZ-Codefeld fünf Ziffern speichert und die Post den PLZ-Code um einen Bindestrich und vier weitere Ziffern erweitert hat. Im schlimmsten Fall könnten Sie das Feld in der Basisklasse so ändern, dass es eine Zeichenfolge aus 10 Zeichen speichert. Jedoch müssten andere Entwickler dann die abgeleiteten Klassen ändern und neu kompilieren, um die neue Größe und den neuen Datentyp zu verwenden.

Der sicherste Weg, eine Basisklasse zu ändern, besteht darin, neue Member hinzuzufügen. Sie können z. B. ein neues Feld hinzufügen, um zusätzliche vier Ziffern im PLZ-Code des vorherigen Beispiels zu speichern. Auf diese Weise können Clientanwendungen so aktualisiert werden, das sie das neue Feld ohne Unterbrechung vorhandener Anwendungen verwenden. Die Möglichkeit, Basisklassen in einer Vererbungshierarchie zu erweitern, ist ein wichtiger Vorteil gegenüber Schnittstellen.

Siehe auch

Konzepte

Verwendungsmöglichkeiten für Schnittstellen

Basisklassen-Entwurfsänderung nach der Bereitstellung