Diese Seite befürwortet Komposition über Vererbung mit dem folgenden Argument (umformuliert in meinen Worten):
Eine Änderung der Signatur einer Methode der Oberklasse (die in der Unterklasse nicht überschrieben wurde) führt an vielen Stellen zu zusätzlichen Änderungen, wenn Vererbung verwendet wird. Wenn wir jedoch Komposition verwenden, befindet sich die erforderliche zusätzliche Änderung nur an einer einzigen Stelle: der Unterklasse.
Ist das wirklich der einzige Grund, die Komposition der Vererbung vorzuziehen? Wenn dies der Fall ist, kann dieses Problem leicht behoben werden, indem ein Codierungsstil erzwungen wird, der das Überschreiben aller Methoden der Oberklasse befürwortet, auch wenn die Unterklasse die Implementierung nicht ändert (dh Dummy-Überschreibungen in die Unterklasse setzt). Vermisse ich hier etwas?
Antworten:
Ich mag Analogien, also hier ist eine: Haben Sie jemals einen dieser Fernseher mit eingebautem Videorecorder gesehen? Wie wäre es mit einem Videorecorder und einem DVD-Player? Oder eine mit Blu-ray, DVD und Videorecorder. Oh und jetzt streamen alle, also müssen wir ein neues TV-Gerät entwerfen ...
Wenn Sie einen Fernseher besitzen, haben Sie höchstwahrscheinlich keinen wie den oben genannten. Wahrscheinlich haben die meisten davon noch nie existiert. Höchstwahrscheinlich verfügen Sie über einen Monitor oder ein Set mit zahlreichen Eingängen, sodass Sie nicht jedes Mal ein neues Set benötigen, wenn es einen neuen Typ von Eingabegerät gibt.
Vererbung ist wie beim Fernseher mit eingebautem Videorecorder. Wenn Sie drei verschiedene Verhaltenstypen haben, die Sie zusammen verwenden möchten, und jeder über zwei Implementierungsoptionen verfügt, benötigen Sie acht verschiedene Klassen, um all diese zu repräsentieren. Wenn Sie weitere Optionen hinzufügen, explodieren die Zahlen. Wenn Sie stattdessen Komposition verwenden, vermeiden Sie dieses kombinatorische Problem, und das Design ist in der Regel besser erweiterbar.
quelle
Nein ist es nicht. Nach meiner Erfahrung ist Komposition über Vererbung das Ergebnis des Denkens an Ihre Software als eine Ansammlung von Objekten mit Verhaltensweisen , die miteinander kommunizieren / Objekte, die Dienste für einander bereitstellen , anstelle von Klassen und Daten .
Auf diese Weise haben Sie einige Objekte, die Dienste bereitstellen. Sie verwenden sie als Bausteine (im Grunde genommen wie Lego), um anderes Verhalten zu ermöglichen (z. B. verwendet ein UserRegistrationService die Dienste, die von einem EmailerService und einem UserRepository bereitgestellt werden ).
Bei der Erstellung größerer Systeme mit diesem Ansatz habe ich in sehr wenigen Fällen natürlich die Vererbung verwendet. Letztendlich ist die Vererbung ein sehr mächtiges Werkzeug, das mit Vorsicht und nur bei Bedarf eingesetzt werden sollte.
quelle
"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 wirCar
,Airplane
,Balloon
undShip
.Hinweis : Wenn Sie dieses Beispiel erden müssen, tun Sie so, als wären dies Objekte in einem Videospiel.
Dann
Car
undAirplane
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 zwischenAirplane
undBalloon
. 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 diesAirplane
eineFlyingVehicule
(Klassenvererbung) ist, aber wir könnten stattdessen sagen, dass diesAirplane
eineFlying
Komponente (Zusammensetzung) undAirplane
eineIFlyingVehicule
(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 komponierenB
?Wenn
A
eine Spezialisierung davonB
das Liskov-Substitutionsprinzip erfüllen soll, ist eine Vererbung möglich oder sogar wünschenswert. Wenn es Situationen gibt, in denenA
kein gültiger Ersatz für vorhandenB
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, dassB
Methoden offengelegt werden, die zu einem ungültigen Status führen könnenA
, sollten wir Komposition anstelle von Vererbung verwenden. Auch wenn wir der Autor von beidem sindB
undA
es eine Sache weniger ist, sich Sorgen zu machen, erleichtert die Komposition die Wiederverwendbarkeit vonB
.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:
- Entwurfsmuster: Elemente wiederverwendbarer objektorientierter Software, Vierergruppe
Nun, das ist eine schlecht designte Elternklasse. Welches ist, warum Sie sollten:
- 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:
Sie können beispielsweise Folgendes auflösen: Ihre Klasse
C
wird nicht von der Klasse erbenB
. Stattdessen hat Ihre KlasseC
ein Element vom TypA
, das ein Objekt vom Typ sein kann oder nichtB
. Auf diese Weise programmieren Sie nicht mit dem Implementierungsdetail vonB
, 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.
quelle
Normalerweise besteht die Grundidee von Composition-Over-Inheritance darin, eine größere Entwurfsflexibilität bereitzustellen und nicht die Menge an Code zu reduzieren, die Sie ändern müssen, um eine Änderung zu propagieren (was ich fragwürdig finde).
Das Beispiel auf Wikipedia gibt ein besseres Beispiel für die Kraft seines Ansatzes:
Wenn Sie für jedes "Kompositionsstück" eine Basisschnittstelle definieren, können Sie auf einfache Weise verschiedene "Kompositionsobjekte" mit einer Vielfalt erstellen, die nur schwer mit der Vererbung zu erreichen ist.
quelle
Die Zeile "Komposition vor Vererbung" ist nichts anderes als ein Schubs in die richtige Richtung. Es wird dich nicht vor deiner eigenen Dummheit bewahren und gibt dir die Chance, die Dinge noch viel schlimmer zu machen. Das liegt daran, dass hier viel Kraft steckt.
Der Hauptgrund dafür ist, dass Programmierer faul sind. So faul, dass sie jede Lösung wählen, die weniger Tastatureingaben bedeutet. Das ist das Hauptverkaufsargument für Vererbung. Ich tippe heute weniger und morgen wird jemand anderes geschraubt. Also, ich will nach Hause gehen.
Am nächsten Tag besteht der Chef darauf, dass ich Komposition benutze. Erklärt nicht warum, besteht aber darauf. Ich nehme also eine Instanz von dem, von dem ich geerbt habe, lege eine Schnittstelle offen, die mit der Schnittstelle identisch ist, von der ich geerbt habe, und implementiere diese Schnittstelle, indem ich die gesamte Arbeit an das Objekt delegiere, von dem ich geerbt habe. Es ist alles hirntote Eingabe, die mit einem Refactoring-Tool hätte gemacht werden können und mir sinnlos erscheint, aber der Chef wollte es, also mache ich es.
Am nächsten Tag frage ich, ob dies das ist, was gesucht wurde. Was glaubst du, sagt der Chef?
Es war eine enorme Zeitverschwendung, es sei denn, es bestand die Notwendigkeit, dynamisch (zur Laufzeit) zu ändern, von was geerbt wurde (siehe Statusmuster). Wie kann der Chef das sagen und trotzdem für eine Komposition vor einer Vererbung sein?
Zusätzlich dazu, dass dies nicht dazu beiträgt, einen Bruch aufgrund von Änderungen der Methodensignatur zu verhindern, wird der größte Vorteil der Komposition gänzlich übersehen. Im Gegensatz zur Vererbung können Sie eine andere Schnittstelle erstellen . Eine, die besser dazu passt, wie es auf dieser Ebene verwendet wird. Weißt du, Abstraktion!
Das automatische Ersetzen der Vererbung durch Komposition und Delegierung hat einige kleinere Vorteile (und einige Nachteile), aber wenn Sie Ihr Gehirn während dieses Vorgangs ausgeschaltet lassen, verpassen Sie eine große Chance.
Indirektion ist sehr mächtig. Benutze es weise.
quelle
Hier ist ein weiterer Grund: In vielen OO-Sprachen (ich denke hier an solche wie C ++, C # oder Java) gibt es keine Möglichkeit, die Klasse eines Objekts zu ändern, nachdem Sie es erstellt haben.
Angenommen, wir erstellen eine Mitarbeiterdatenbank. Wir haben eine abstrakte Basisklasse für Mitarbeiter und beginnen, neue Klassen für bestimmte Rollen abzuleiten - Ingenieur, Wachmann, LKW-Fahrer und so weiter. Jede Klasse hat ein spezielles Verhalten für diese Rolle. Alles ist gut.
Dann wird eines Tages ein Ingenieur zum Engineering Manager befördert. Sie können einen vorhandenen Ingenieur nicht neu klassifizieren. Stattdessen müssen Sie eine Funktion schreiben, die einen Engineering Manager erstellt, alle Daten aus dem Engineer kopiert, den Engineer aus der Datenbank löscht und dann den neuen Engineering Manager hinzufügt. Und Sie müssen eine solche Funktion für jeden möglichen Rollenwechsel schreiben.
Schlimmer noch: Angenommen, ein Lkw-Fahrer ist langfristig krankgeschrieben, und ein Wachmann bietet an, in Teilzeit Lkw-Fahren zu absolvieren. Jetzt müssen Sie eine neue Klasse für Wachmann und Lkw-Fahrer erfinden.
Es macht das Leben so viel einfacher, eine konkrete Employee-Klasse zu erstellen und sie als Container zu behandeln, dem Sie Eigenschaften wie die Jobrolle hinzufügen können.
quelle
Vererbung bietet zwei Dinge:
1) Code-Wiederverwendung: Kann leicht durch Komposition erreicht werden. Tatsächlich wird dies besser durch Komposition erreicht, da die Einkapselung besser erhalten bleibt.
2) Polymorphismus: Kann durch "Interfaces" (Klassen, in denen alle Methoden rein virtuell sind) erreicht werden.
Ein starkes Argument für die Verwendung der Nicht-Schnittstellenvererbung ist, wenn die Anzahl der erforderlichen Methoden groß ist und Ihre Unterklasse nur eine kleine Teilmenge von ihnen ändern möchte. Im Allgemeinen sollte Ihre Benutzeroberfläche jedoch nur einen sehr geringen Platzbedarf haben, wenn Sie sich dem Prinzip der "Einzelverantwortung" anschließen (Sie sollten dies tun). Daher sollten diese Fälle selten vorkommen.
Der Codierungsstil, der das Überschreiben aller Methoden der übergeordneten Klasse erfordert, vermeidet es nicht, eine große Anzahl von Passthrough-Methoden zu schreiben. Warum also nicht Code durch Komposition wiederverwenden und den zusätzlichen Vorteil der Kapselung nutzen? Wenn die Superklasse durch Hinzufügen einer weiteren Methode erweitert würde, müsste diese Methode für den Codierungsstil allen Unterklassen hinzugefügt werden (und es besteht eine gute Chance, dass wir dann gegen die "Schnittstellentrennung" verstoßen ). Dieses Problem wächst schnell, wenn Sie mehrere Ebenen in der Vererbung haben.
Schließlich ist es in vielen Sprachen viel einfacher, auf "Komposition" basierende Objekte auf Unit-Tests zu testen, als auf "Vererbung" basierende Objekte auf Unit-Tests, indem das Mitgliedsobjekt durch ein Schein- / Test- / Dummy-Objekt ersetzt wird.
quelle
Nee.
Es gibt viele Gründe, die Zusammensetzung der Vererbung vorzuziehen. Es gibt keine einzige richtige Antwort. Aber ich werde der Mischung einen anderen Grund vorziehen, Komposition gegenüber Vererbung zu bevorzugen:
Durch die Bevorzugung der Komposition gegenüber der Vererbung wird die Lesbarkeit Ihres Codes verbessert ist sehr wichtig, wenn Sie an einem großen Projekt mit mehreren Personen arbeiten.
So denke ich darüber nach: Wenn ich den Code einer anderen Person lese und sehe, dass sie eine Klasse erweitert hat, werde ich sie fragen welches Verhalten in der Superklasse sie ändert.
Und dann gehe ich den Code durch und suche nach einer Überschreibungsfunktion. Wenn ich keine sehe, klassifiziere ich das als Missbrauch der Vererbung. Sie hätten stattdessen Kompositionen verwenden sollen, um mich (und jede andere Person im Team) vor 5 Minuten Lesen des Codes zu bewahren!
Hier ein konkretes Beispiel: In Java
JFrame
repräsentiert die Klasse ein Fenster. Um ein Fenster zu erstellen, erstellen Sie eine Instanz vonJFrame
und rufen dann Funktionen für diese Instanz auf, um ihr Inhalte hinzuzufügen und sie anzuzeigen.In vielen Tutorials (auch den offiziellen) wird empfohlen, diese zu erweitern,
JFrame
damit Sie Instanzen Ihrer Klasse als Instanzen von behandeln könnenJFrame
. Aber imho (und die Meinung vieler anderer) ist, dass es eine ziemlich schlechte Angewohnheit ist, sich darauf einzulassen! Stattdessen sollten Sie nur eine Instanz von erstellenJFrame
und Funktionen für diese Instanz aufrufen.JFrame
In diesem Fall ist eine Erweiterung absolut nicht erforderlich , da Sie das Standardverhalten der übergeordneten Klasse nicht ändern.Auf der anderen Seite besteht eine Möglichkeit zum benutzerdefinierten Zeichnen darin, die
JPanel
Klasse zu erweitern und diepaintComponent()
Funktion zu überschreiben . Dies ist eine gute Verwendung der Vererbung. Wenn ich Ihren Code durchsuche und sehe, dass SieJPanel
diepaintComponent()
Funktion erweitert und überschrieben haben , weiß ich genau, welches Verhalten Sie ändern.Wenn Sie nur Code selbst schreiben, ist die Verwendung der Vererbung möglicherweise genauso einfach wie die Verwendung der Komposition. Stellen Sie sich jedoch vor, Sie befinden sich in einer Gruppenumgebung und andere Personen lesen Ihren Code. Durch die Bevorzugung der Komposition gegenüber der Vererbung wird der Code leichter lesbar.
quelle
new
Schlüsselwort gibt Ihnen einen. Trotzdem danke für das Gespräch.