"Komposition gegenüber Vererbung bevorzugen" ist nur eine gute Heuristik
Sie sollten den Kontext berücksichtigen, da dies keine universelle Regel ist. Nehmen Sie das nicht so, als würden Sie niemals Vererbung verwenden, wenn Sie Kompositionen erstellen können. In diesem Fall können Sie das Problem beheben, indem Sie die Vererbung verbieten.
Ich hoffe, dass dieser Punkt in diesem Beitrag geklärt wird.
Ich werde nicht versuchen, die Verdienste der Komposition allein zu verteidigen. Das halte ich nicht für ein Thema. Stattdessen werde ich auf einige Situationen eingehen, in denen Entwickler eine Vererbung in Betracht ziehen, bei der die Komposition besser geeignet ist. In diesem Sinne hat die Vererbung ihre eigenen Vorzüge, die ich auch als nicht thematisch betrachte.
Fahrzeugbeispiel
Ich schreibe über Entwickler, die versuchen, Dinge auf dumme Weise zu erzählen
Lassen Sie uns für eine Variante der klassischen Beispiele gehen , dass einige OOP Kurse nutzen ... Wir haben eine Vehicle
Klasse, dann leiten wir Car
, Airplane
, Balloon
undShip
.
Hinweis : Wenn Sie dieses Beispiel erden müssen, tun Sie so, als wären dies Objekte in einem Videospiel.
Dann Car
und Airplane
vielleicht haben sie einen gemeinsamen Code, weil sie beide an Land auf dem Rad rollen können. Die Entwickler können in Betracht ziehen, dafür eine Zwischenklasse in der Vererbungskette zu erstellen. Tatsächlich gibt es aber auch einen gemeinsamen Code zwischen Airplane
und Balloon
. Sie könnten in Betracht ziehen, dafür eine weitere Zwischenklasse in der Vererbungskette zu erstellen.
Daher würde der Entwickler eine Mehrfachvererbung anstreben. An dem Punkt, an dem die Entwickler nach Mehrfachvererbung suchen, ist das Design bereits schiefgelaufen.
Es ist besser, dieses Verhalten als Schnittstellen und Komposition zu modellieren, damit wir es wiederverwenden können, ohne auf die Vererbung mehrerer Klassen stoßen zu müssen. Wenn die Entwickler beispielsweise die FlyingVehicule
Klasse erstellen . Sie würden sagen, dass dies Airplane
eine FlyingVehicule
(Klassenvererbung) ist, aber wir könnten stattdessen sagen, dass dies Airplane
eine Flying
Komponente (Zusammensetzung) und Airplane
eine IFlyingVehicule
(Schnittstellenvererbung) ist.
Falls erforderlich, können wir Schnittstellen mehrfach vererben (von Schnittstellen). Darüber hinaus koppeln Sie nicht an eine bestimmte Implementierung. Verbesserte Wiederverwendbarkeit und Testbarkeit Ihres Codes.
Denken Sie daran, dass Vererbung ein Werkzeug für Polymorphismus ist. Darüber hinaus ist Polymorphismus ein Werkzeug für die Wiederverwendbarkeit. Wenn Sie die Wiederverwendbarkeit Ihres Codes mithilfe der Komposition erhöhen können, tun Sie dies. Wenn Sie sich nicht sicher sind, ob die Komposition eine bessere Wiederverwendbarkeit bietet, ist "Komposition der Vererbung vorziehen" eine gute Heuristik.
Das alles ohne zu erwähnen Amphibious
.
Tatsächlich brauchen wir möglicherweise keine Dinge, die vom Boden abgehen. Stephen Hurn hat ein beredteres Beispiel in seinen Artikeln "Favor Composition Over Inheritance" Teil 1 und Teil 2 .
Substituierbarkeit und Verkapselung
Soll A
erben oder komponieren B
?
Wenn A
eine Spezialisierung davon B
das Liskov-Substitutionsprinzip erfüllen soll, ist eine Vererbung möglich oder sogar wünschenswert. Wenn es Situationen gibt, in denen A
kein gültiger Ersatz für vorhanden B
ist, sollten wir keine Vererbung verwenden.
Wir könnten an Komposition als Form der defensiven Programmierung interessiert sein, um die abgeleitete Klasse zu verteidigen . Insbesondere wenn Sie mit der Verwendung B
für andere Zwecke beginnen, besteht möglicherweise der Druck, sie zu ändern oder zu erweitern, um sie für diese Zwecke besser geeignet zu machen. Wenn das Risiko besteht, dass B
Methoden offengelegt werden, die zu einem ungültigen Status führen können A
, sollten wir Komposition anstelle von Vererbung verwenden. Auch wenn wir der Autor von beidem sind B
und A
es eine Sache weniger ist, sich Sorgen zu machen, erleichtert die Komposition die Wiederverwendbarkeit von B
.
Wir können sogar argumentieren, dass, wenn es Features gibt B
, A
die nicht benötigt werden (und wir nicht wissen, ob diese Features zu einem ungültigen Status für führen könntenA
es eine gute Idee ist, Komposition zu verwenden , weder in der gegenwärtigen noch in der Zukunft) statt vererbung.
Die Komposition hat auch die Vorteile, dass sie das Umschalten von Implementierungen ermöglicht und das Verspotten erleichtert.
Hinweis : Es gibt Situationen, in denen wir Komposition verwenden möchten, obwohl die Ersetzung gültig ist. Wir archivieren diese Substituierbarkeit mithilfe von Interfaces oder abstrakten Klassen (welche verwendet werden sollen, wenn es sich um ein anderes Thema handelt) und verwenden dann die Komposition mit Abhängigkeitsinjektion der realen Implementierung.
Schließlich gibt es natürlich das Argument, dass wir Komposition verwenden sollten , um die übergeordnete Klasse zu verteidigen, da die Vererbung die Kapselung der übergeordneten Klasse unterbricht:
Durch die Vererbung wird eine Unterklasse den Details der Implementierung der übergeordneten Klasse ausgesetzt. Es wird häufig gesagt, dass durch die Vererbung die Kapselung unterbrochen wird.
- Entwurfsmuster: Elemente wiederverwendbarer objektorientierter Software, Vierergruppe
Nun, das ist eine schlecht designte Elternklasse. Welches ist, warum Sie sollten:
Entwerfen Sie für die Vererbung oder verbieten Sie es.
- Wirksames Java, Josh Bloch
Jo-Jo-Problem
Ein weiterer Fall, in dem Komposition hilft, ist das Jo-Jo-Problem . Dies ist ein Zitat aus Wikipedia:
In der Softwareentwicklung ist das Jojo-Problem ein Anti-Muster, das auftritt, wenn ein Programmierer ein Programm lesen und verstehen muss, dessen Vererbungsgraph so lang und kompliziert ist, dass der Programmierer ständig zwischen vielen verschiedenen Klassendefinitionen wechseln muss, um zu folgen der Kontrollfluss des Programms.
Sie können beispielsweise Folgendes auflösen: Ihre Klasse C
wird nicht von der Klasse erben B
. Stattdessen hat Ihre Klasse C
ein Element vom Typ A
, das ein Objekt vom Typ sein kann oder nicht B
. Auf diese Weise programmieren Sie nicht mit dem Implementierungsdetail von B
, sondern mit dem Vertrag, den die Schnittstelle (von) A
anbietet.
Gegenbeispiele
Viele Frameworks bevorzugen die Vererbung gegenüber der Komposition (was das Gegenteil von dem ist, was wir argumentiert haben). Entwickler tun dies möglicherweise, weil sie viel Arbeit in ihre Basisklasse stecken, weil die Implementierung mit Komposition die Größe des Clientcodes erhöht hätte. Manchmal liegt dies an sprachlichen Einschränkungen.
Beispielsweise kann ein PHP-ORM-Framework eine Basisklasse erstellen, die mithilfe magischer Methoden das Schreiben von Code ermöglicht, als ob das Objekt echte Eigenschaften hätte. Stattdessen wird der Code, der von den Magic-Methoden verarbeitet wird, in die Datenbank verschoben, nach dem bestimmten angeforderten Feld abgefragt (möglicherweise für zukünftige Anforderungen zwischengespeichert) und zurückgegeben. Bei der Komposition muss der Client entweder Eigenschaften für jedes Feld erstellen oder eine Version des Codes für magische Methoden schreiben.
Nachtrag : Es gibt noch einige andere Möglichkeiten, ORM-Objekte zu erweitern. Daher halte ich eine Vererbung in diesem Fall nicht für erforderlich. Es ist günstiger.
In einem anderen Beispiel kann eine Videospiel-Engine eine Basisklasse erstellen, die je nach Zielplattform systemeigenen Code verwendet, um 3D-Rendering und Ereignisbehandlung durchzuführen. Dieser Code ist komplex und plattformspezifisch. Es wäre teuer und fehleranfällig für den Entwickler des Moduls, sich mit diesem Code zu befassen, was in der Tat ein Grund für die Verwendung des Moduls ist.
Darüber hinaus funktionieren ohne den 3D-Rendering-Teil so viele Widget-Frameworks. Dies befreit Sie von der Sorge, mit Betriebssystemnachrichten umzugehen. In vielen Sprachen können Sie keinen solchen Code schreiben, ohne eine Form von systemeigenem Gebot zu haben. Wenn Sie dies tun würden, würde dies außerdem Ihre Portabilität beeinträchtigen. Stattdessen mit Vererbung, vorausgesetzt, der Entwickler unterbricht die Kompatibilität nicht (zu stark); Sie können Ihren Code problemlos auf neue Plattformen portieren, die sie in Zukunft unterstützen.
Bedenken Sie außerdem, dass wir häufig nur einige Methoden überschreiben und alles andere mit den Standardimplementierungen belassen möchten. Wenn wir Komposition verwendet hätten, müssten wir alle diese Methoden erstellen, auch wenn wir nur an das umschlossene Objekt delegieren würden.
Durch dieses Argument gibt es einen Punkt, an dem die Komposition für die Wartbarkeit schlechter ist als die Vererbung (wenn die Basisklasse zu komplex ist). Denken Sie jedoch daran, dass die Wartbarkeit der Vererbung schlechter sein kann als die der Komposition (wenn der Vererbungsbaum zu komplex ist), worauf ich im Jojo-Problem Bezug nehme.
In den vorgestellten Beispielen beabsichtigen die Entwickler selten, den durch Vererbung generierten Code in anderen Projekten wiederzuverwenden. Dies verringert die verringerte Wiederverwendbarkeit der Verwendung von Vererbung anstelle von Komposition. Durch die Verwendung der Vererbung können die Framework-Entwickler außerdem eine Menge einfach zu verwendenden und leicht zu erkennenden Codes bereitstellen.
Abschließende Gedanken
Wie Sie sehen, hat die Komposition in einigen Situationen, nicht immer, einen gewissen Vorteil gegenüber der Vererbung. Es ist wichtig, den Kontext und die verschiedenen Faktoren (wie Wiederverwendbarkeit, Wartbarkeit, Testbarkeit usw.) zu berücksichtigen, um die Entscheidung zu treffen. Zurück zum ersten Punkt: „Bevorzugen Komposition der Vererbung“ ist eine nur gute Heuristik.
Möglicherweise stellen Sie auch fest, dass viele der von mir beschriebenen Situationen bis zu einem gewissen Grad mit Traits oder Mixins gelöst werden können. Leider sind diese Funktionen in der großen Liste der Sprachen nicht alltäglich und verursachen in der Regel einige Leistungskosten. Zum Glück entschärfen ihre beliebten Erweiterungsmethoden und Standardimplementierungen einige Situationen.
Ich habe kürzlich einen Beitrag veröffentlicht, in dem ich einige der Vorteile von Schnittstellen erläutere. Warum benötigen wir Schnittstellen zwischen Benutzeroberfläche, Unternehmen und Datenzugriff in C # ? Es hilft beim Entkoppeln und erleichtert die Wiederverwendbarkeit und Testbarkeit.