Komposition lieber als Vererbung?


1604

Warum Komposition gegenüber Vererbung bevorzugen? Welche Kompromisse gibt es für jeden Ansatz? Wann sollten Sie die Vererbung der Komposition vorziehen?



40
Zu dieser Frage gibt es hier einen guten Artikel . Meine persönliche Meinung ist, dass es kein "besseres" oder "schlechteres" Prinzip gibt. Es gibt ein "angemessenes" und "unangemessenes" Design für die konkrete Aufgabe. Mit anderen Worten - ich verwende je nach Situation sowohl Vererbung als auch Komposition. Ziel ist es, kleineren Code zu erstellen, der einfacher zu lesen, wiederzuverwenden und schließlich weiter zu erweitern ist.
m_pGladiator

1
In einem Satz ist die Vererbung öffentlich, wenn Sie eine öffentliche Methode haben und diese ändern. Dadurch wird die veröffentlichte API geändert. Wenn Sie eine Komposition haben und sich das zusammengesetzte Objekt geändert hat, müssen Sie Ihre veröffentlichte API nicht ändern.
Tomer Ben David

Antworten:


1188

Ziehen Sie die Komposition der Vererbung vor, da sie später formbarer / einfacher zu ändern ist, verwenden Sie jedoch keinen Compose-Always-Ansatz. Mit der Komposition ist es einfach, das Verhalten im laufenden Betrieb mit Dependency Injection / Setter zu ändern. Die Vererbung ist starrer, da die meisten Sprachen es Ihnen nicht erlauben, von mehr als einem Typ abzuleiten. So wird die Gans mehr oder weniger gekocht, sobald Sie von Typ A abgeleitet sind.

Mein Härtetest für die oben genannten ist:

  • Möchte TypeB die gesamte Schnittstelle (nicht weniger alle öffentlichen Methoden) von TypeA verfügbar machen, sodass TypeB dort verwendet werden kann, wo TypeA erwartet wird? Zeigt Vererbung an .

    • Beispiel: Ein Cessna-Doppeldecker legt die gesamte Schnittstelle eines Flugzeugs frei, wenn nicht sogar mehr. Das macht es also passend, vom Flugzeug abzuleiten.
  • Will TypeB nur einen Teil des von TypeA offenbarten Verhaltens? Zeigt den Bedarf an Zusammensetzung an.

    • Beispielsweise benötigt ein Vogel möglicherweise nur das Flugverhalten eines Flugzeugs. In diesem Fall ist es sinnvoll, es als Schnittstelle / Klasse / beide zu extrahieren und es zu einem Mitglied beider Klassen zu machen.

Update: Ich bin gerade auf meine Antwort zurückgekommen und es scheint, dass sie unvollständig ist, ohne das Liskov-Substitutionsprinzip von Barbara Liskov als Test für "Soll ich von diesem Typ erben?"


81
Das zweite Beispiel stammt direkt aus dem Buch Head First Design Patterns ( amazon.com/First-Design-Patterns-Elisabeth-Freeman/dp/… ) :) Ich würde dieses Buch jedem empfehlen, der diese Frage gegoogelt hat.
Jeshurun

4
Es ist sehr klar, aber es kann etwas fehlen: "Möchte TypeB die gesamte Schnittstelle (nicht weniger alle öffentlichen Methoden) von TypeA verfügbar machen, sodass TypeB dort verwendet werden kann, wo TypeA erwartet wird?" Aber was ist, wenn dies zutrifft und TypeB auch die gesamte Schnittstelle von TypeC verfügbar macht? Und was ist, wenn TypeC noch nicht modelliert wurde?
Tristan

4
Sie spielen auf das an, was meiner Meinung nach der grundlegendste Test sein sollte: "Sollte dieses Objekt von Code verwendet werden können, der Objekte vom Basistyp erwartet (was wäre das?)". Wenn die Antwort Ja lautet, muss das Objekt erben. Wenn nein, dann sollte es wahrscheinlich nicht. Wenn ich meine Druthers hätte, würden Sprachen ein Schlüsselwort bereitstellen, um auf "diese Klasse" zu verweisen, und ein Mittel zum Definieren einer Klasse bereitstellen, die sich wie eine andere Klasse verhalten sollte, aber nicht durch diese ersetzt werden kann (eine solche Klasse hätte all dies " Klasse "Referenzen durch sich selbst ersetzt).
Supercat

22
@Alexey - der Punkt ist "Kann ich einen Cessna-Doppeldecker an alle Kunden weitergeben, die ein Flugzeug erwarten, ohne sie zu überraschen?". Wenn ja, möchten Sie wahrscheinlich eine Vererbung.
Gishu

9
Ich kämpfe tatsächlich darum, Beispiele zu finden, bei denen Vererbung meine Antwort gewesen wäre. Ich finde oft, dass Aggregation, Zusammensetzung und Schnittstellen zu eleganteren Lösungen führen. Viele der obigen Beispiele könnten möglicherweise besser mit diesen Ansätzen erklärt werden ...
Stuart Wakefield

414

Stellen Sie sich Containment als a vor Beziehung vor. Ein Auto "hat einen" Motor, eine Person "hat einen" Namen usw. "

Stellen Sie sich Vererbung als a vor Beziehung vor. Ein Auto ist ein Fahrzeug, eine Person ist ein Säugetier usw.

Ich nehme diesen Ansatz nicht zur Kenntnis. Ich habe es direkt aus der zweiten Ausgabe von Code Complete von Steve McConnell , Abschnitt 6.3 , entnommen .


107
Dies ist nicht immer ein perfekter Ansatz, sondern lediglich eine gute Richtlinie. Das Liskov-Substitutionsprinzip ist viel genauer (scheitert weniger).
Bill K

40
"Mein Auto hat ein Fahrzeug." Wenn Sie das als separaten Satz betrachten, nicht in einem Programmierkontext, macht das absolut keinen Sinn. Und das ist der springende Punkt dieser Technik. Wenn es unangenehm klingt, ist es wahrscheinlich falsch.
Nick Zalutskiy

36
@ Nick Sicher, aber "Mein Auto hat ein Fahrzeugverhalten" macht mehr Sinn (ich denke, Ihre "Fahrzeug" -Klasse könnte "Fahrzeugverhalten" heißen). Sie können Ihre Entscheidung also nicht auf "hat ein" vs "ist ein" Vergleich, Sie müssen LSP verwenden, oder Sie werden Fehler machen
Tristan

35
Anstelle von "ist ein" Denken Sie an "verhält sich wie". Bei der Vererbung geht es darum, Verhalten zu erben, nicht nur um Semantik.
Ybakos

4
@ybakos "Benimmt sich wie" kann über Schnittstellen ohne Vererbung erreicht werden. Aus Wikipedia : "Eine Implementierung von Komposition über Vererbung beginnt normalerweise mit der Erstellung verschiedener Schnittstellen, die die Verhaltensweisen darstellen, die das System aufweisen muss ... Somit werden Systemverhalten ohne Vererbung realisiert."
DavidRR

210

Wenn Sie den Unterschied verstehen, ist es einfacher zu erklären.

Verfahrensordnung

Ein Beispiel hierfür ist PHP ohne Verwendung von Klassen (insbesondere vor PHP5). Die gesamte Logik ist in einer Reihe von Funktionen codiert. Sie können andere Dateien einschließen, die Hilfsfunktionen usw. enthalten, und Ihre Geschäftslogik ausführen, indem Sie Daten in Funktionen weitergeben. Dies kann sehr schwierig zu verwalten sein, wenn die Anwendung wächst. PHP5 versucht dies zu beheben, indem es ein objektorientierteres Design bietet.

Erbe

Dies fördert die Verwendung von Klassen. Vererbung ist einer der drei Grundsätze des OO-Designs (Vererbung, Polymorphismus, Einkapselung).

class Person {
   String Title;
   String Name;
   Int Age
}

class Employee : Person {
   Int Salary;
   String Title;
}

Dies ist Vererbung bei der Arbeit. Der Mitarbeiter ist eine Person oder erbt von der Person. Alle Vererbungsbeziehungen sind "is-a" -Beziehungen. Der Mitarbeiter schattiert auch die Title-Eigenschaft von Person, was bedeutet, dass Employee.Title den Titel für den Mitarbeiter und nicht für die Person zurückgibt.

Komposition

Die Zusammensetzung wird der Vererbung vorgezogen. Um es ganz einfach auszudrücken: Sie hätten:

class Person {
   String Title;
   String Name;
   Int Age;

   public Person(String title, String name, String age) {
      this.Title = title;
      this.Name = name;
      this.Age = age;
   }

}

class Employee {
   Int Salary;
   private Person person;

   public Employee(Person p, Int salary) {
       this.person = p;
       this.Salary = salary;
   }
}

Person johnny = new Person ("Mr.", "John", 25);
Employee john = new Employee (johnny, 50000);

Die Zusammensetzung ist typischerweise "hat eine" oder "verwendet eine" Beziehung. Hier hat die Employee-Klasse eine Person. Es erbt nicht von Person, sondern erhält das an ihn übergebene Person-Objekt, weshalb es eine Person "hat".

Zusammensetzung über Vererbung

Angenommen, Sie möchten einen Manager-Typ erstellen, um Folgendes zu erhalten:

class Manager : Person, Employee {
   ...
}

Dieses Beispiel funktioniert jedoch einwandfrei. Was passiert, wenn Person und Mitarbeiter beide deklariert haben Title? Sollte Manager.Title "Manager of Operations" oder "Mr." zurückgeben? Unter Komposition wird diese Mehrdeutigkeit besser behandelt:

Class Manager {
   public string Title;
   public Manager(Person p, Employee e)
   {
      this.Title = e.Title;
   }
}

Das Manager-Objekt besteht aus einem Mitarbeiter und einer Person. Das Titelverhalten wird vom Mitarbeiter übernommen. Diese explizite Komposition beseitigt unter anderem Mehrdeutigkeiten und es treten weniger Fehler auf.


6
Für die Vererbung: Es gibt keine Mehrdeutigkeit. Sie implementieren die Manager-Klasse basierend auf den Anforderungen. Sie würden also "Manager of Operations" zurückgeben, wenn dies Ihren Anforderungen entspricht, andernfalls würden Sie nur die Implementierung der Basisklasse verwenden. Sie können Person auch zu einer abstrakten Klasse machen und dadurch sicherstellen, dass nachgeschaltete Klassen eine Title-Eigenschaft implementieren.
Raj Rao

68
Es ist wichtig, sich daran zu erinnern, dass man "Zusammensetzung über Vererbung" sagen könnte, aber das bedeutet nicht "Zusammensetzung immer über Vererbung". "Ist ein" bedeutet Vererbung und führt zur Wiederverwendung von Code. Mitarbeiter ist eine Person (Mitarbeiter hat keine Person).
Raj Rao

36
Das Beispiel ist verwirrend. Der Mitarbeiter ist eine Person, daher sollte er die Vererbung verwenden. Sie sollten für dieses Beispiel keine Komposition verwenden, da es sich um eine falsche Beziehung im Domänenmodell handelt, auch wenn Sie sie technisch im Code deklarieren können.
Michael Freidgeim

15
Ich bin mit diesem Beispiel nicht einverstanden. Ein Mitarbeiter ist eine Person, bei der es sich um ein Lehrbuch handelt, in dem die Vererbung ordnungsgemäß verwendet wird. Ich denke auch, dass das "Problem" der Neudefinition des Titelfeldes keinen Sinn ergibt. Die Tatsache, dass Employee.Title Person.Title beschattet, ist ein Zeichen für schlechte Programmierung. Immerhin sind "Mr." und "Manager of Operations" beziehen sich wirklich auf den gleichen Aspekt einer Person (Kleinbuchstaben)? Ich würde Employee.Title umbenennen und somit auf die Attribute Title und JobTitle eines Mitarbeiters verweisen können, die beide im wirklichen Leben sinnvoll sind. Darüber hinaus gibt es keinen Grund für Manager (Fortsetzung ...)
Radon Rosborough

9
(... Fortsetzung) von Person und Mitarbeiter erben - schließlich erbt Mitarbeiter bereits von Person. In komplexeren Modellen, in denen eine Person ein Manager und ein Agent sein kann, kann zwar die Mehrfachvererbung verwendet werden (sorgfältig!), In vielen Umgebungen ist es jedoch vorzuziehen, eine abstrakte Rollenklasse zu haben, aus der der Manager (Mitarbeiter enthält) besteht er / sie verwaltet) und Agent (enthält Verträge und andere Informationen) erben. Dann ist ein Mitarbeiter eine Person mit mehreren Rollen. Somit werden sowohl Zusammensetzung als auch Vererbung richtig verwendet.
Radon Rosborough

142

Bei all den unbestreitbaren Vorteilen, die die Vererbung bietet, sind hier einige ihrer Nachteile aufgeführt.

Nachteile der Vererbung:

  1. Sie können die von Superklassen zur Laufzeit geerbte Implementierung nicht ändern (offensichtlich, weil die Vererbung zur Kompilierungszeit definiert ist).
  2. Durch die Vererbung wird eine Unterklasse Details der Implementierung ihrer übergeordneten Klasse ausgesetzt. Aus diesem Grund wird häufig gesagt, dass die Vererbung die Kapselung unterbricht (in dem Sinne, dass Sie sich wirklich nur auf Schnittstellen und nicht auf die Implementierung konzentrieren müssen, sodass die Wiederverwendung durch Unterklassifizierung nicht immer bevorzugt wird).
  3. Die durch die Vererbung bereitgestellte enge Kopplung macht die Implementierung einer Unterklasse sehr eng mit der Implementierung einer Superklasse verbunden, sodass jede Änderung in der übergeordneten Implementierung die Unterklasse zur Änderung zwingt.
  4. Übermäßige Wiederverwendung durch Unterklassifizierung kann den Vererbungsstapel sehr tief und auch sehr verwirrend machen.

Andererseits wird die Objektzusammensetzung zur Laufzeit durch Objekte definiert, die Verweise auf andere Objekte erhalten. In einem solchen Fall können diese Objekte niemals die geschützten Daten des anderen erreichen (keine Kapselungsunterbrechung) und sind gezwungen, die Schnittstelle des anderen zu respektieren. Auch in diesem Fall sind die Implementierungsabhängigkeiten viel geringer als bei der Vererbung.


5
Dies ist meiner Meinung nach eine der besseren Antworten - ich möchte noch hinzufügen, dass der Versuch, Ihre Probleme in Bezug auf die Komposition zu überdenken, meiner Erfahrung nach dazu führt, dass kleinere, einfachere, in sich geschlossenere und wiederverwendbarere Klassen entstehen mit einem klareren, kleineren und fokussierteren Verantwortungsbereich. Oft bedeutet dies, dass weniger Dinge wie Abhängigkeitsinjektion oder Verspottung (in Tests) erforderlich sind, da kleinere Komponenten normalerweise für sich alleine stehen können. Nur meine Erfahrung. YMMV :-)
mindplay.dk

3
Der letzte Absatz in diesem Beitrag hat wirklich für mich geklickt. Vielen Dank.
Salx

87

Ein weiterer, sehr pragmatischer Grund, die Komposition der Vererbung vorzuziehen, hat mit Ihrem Domänenmodell zu tun und es einer relationalen Datenbank zuzuordnen. Es ist wirklich schwierig, die Vererbung dem SQL-Modell zuzuordnen (am Ende gibt es alle möglichen hackigen Problemumgehungen, z. B. das Erstellen von Spalten, die nicht immer verwendet werden, mithilfe von Ansichten usw.). Einige ORMLs versuchen damit umzugehen, aber es wird immer schnell kompliziert. Die Zusammensetzung kann leicht durch eine Fremdschlüsselbeziehung zwischen zwei Tabellen modelliert werden, die Vererbung ist jedoch viel schwieriger.


81

Während ich in kurzen Worten "Komposition lieber als Vererbung bevorzugen" zustimmen würde, klingt es für mich sehr oft wie "Kartoffeln gegenüber Coca-Cola bevorzugen". Es gibt Orte für die Vererbung und Orte für die Komposition. Sie müssen den Unterschied verstehen, dann verschwindet diese Frage. Was es für mich wirklich bedeutet, ist "wenn Sie Vererbung verwenden wollen - denken Sie noch einmal darüber nach, wahrscheinlich brauchen Sie Komposition".

Sie sollten Kartoffeln gegenüber Coca Cola bevorzugen, wenn Sie essen möchten, und Coca Cola gegenüber Kartoffeln, wenn Sie trinken möchten.

Das Erstellen einer Unterklasse sollte mehr als nur eine bequeme Möglichkeit zum Aufrufen von Methoden der Oberklasse bedeuten. Sie sollten die Vererbung verwenden, wenn die Unterklasse sowohl strukturell als auch funktional eine Superklasse ist, wenn sie als Oberklasse verwendet werden kann und Sie diese verwenden werden. Wenn dies nicht der Fall ist, handelt es sich nicht um eine Vererbung, sondern um etwas anderes. Zusammensetzung ist, wenn Ihre Objekte aus einem anderen bestehen oder eine Beziehung zu ihnen haben.

Für mich sieht es so aus, als ob jemand nicht weiß, ob er Vererbung oder Zusammensetzung benötigt. Das eigentliche Problem ist, dass er nicht weiß, ob er trinken oder essen möchte. Denken Sie mehr über Ihre Problemdomäne nach und verstehen Sie sie besser.


5
Das richtige Werkzeug für den richtigen Job. Ein Hammer kann zwar besser auf Dinge einschlagen als ein Schraubenschlüssel, aber das bedeutet nicht, dass man einen Schraubenschlüssel als "minderwertigen Hammer" ansehen sollte. Vererbung kann hilfreich sein, wenn die Dinge, die der Unterklasse hinzugefügt werden, erforderlich sind, damit sich das Objekt als Objekt der Oberklasse verhält. Betrachten Sie beispielsweise eine Basisklasse InternalCombustionEnginemit einer abgeleiteten Klasse GasolineEngine. Letzteres fügt Dinge wie Zündkerzen hinzu, die der Basisklasse fehlen, aber wenn Sie das Ding als verwenden, InternalCombustionEnginewerden die Zündkerzen verwendet.
Supercat

61

Vererbung ist ziemlich verlockend, besonders wenn sie aus dem prozeduralen Land stammt, und sie sieht oft täuschend elegant aus. Ich meine, alles was ich tun muss, ist diese eine Funktionalität einer anderen Klasse hinzuzufügen, oder? Nun, eines der Probleme ist das

Vererbung ist wahrscheinlich die schlechteste Form der Kopplung, die Sie haben können

Ihre Basisklasse unterbricht die Kapselung, indem sie Implementierungsdetails in Form geschützter Mitglieder Unterklassen zur Verfügung stellt. Dies macht Ihr System starr und zerbrechlich. Der tragischere Fehler ist jedoch, dass die neue Unterklasse das gesamte Gepäck und die Meinung der Erbschaftskette mit sich bringt.

Der Artikel Vererbung ist böse: Der epische Fehler des DataAnnotationsModelBinder führt ein Beispiel in C # durch. Es zeigt die Verwendung der Vererbung, wann die Komposition hätte verwendet werden sollen und wie sie überarbeitet werden könnte.


Vererbung ist nicht gut oder schlecht, es ist nur ein Sonderfall der Komposition. Wobei die Unterklasse tatsächlich eine ähnliche Funktionalität wie die Oberklasse implementiert. Wenn Ihr Vorschlag Unterklasse nicht Neuimplementierung ist , sondern nur mit der Funktionalität der übergeordneten Klasse, dann haben Sie falsch verwendet Vererbung. Das ist der Fehler des Programmierers, keine Reflexion über die Vererbung.
iPherian

42

In Java oder C # kann ein Objekt seinen Typ nicht ändern, sobald es instanziiert wurde.

Also, wenn Ihr Objekt Notwendigkeit als ein anderes Objekt erscheinen oder sich anders verhalten auf einem Objektzustand oder Bedingungen abhängig, dann verwenden Zusammensetzung : Siehe Staat und Strategie Design Patterns.

Wenn das Objekt vom selben Typ sein muss, verwenden Sie Vererbung oder implementieren Sie Schnittstellen.


10
+1 Ich habe immer weniger festgestellt, dass Vererbung in den meisten Situationen funktioniert. Ich bevorzuge gemeinsam genutzte / geerbte Schnittstellen und die Zusammensetzung von Objekten ... oder heißt es Aggregation? Frag mich nicht, ich habe einen EE-Abschluss !!
Kenny

Ich glaube, dass dies das häufigste Szenario ist, in dem "Zusammensetzung über Vererbung" gilt, da beide theoretisch passen könnten. In einem Marketing-System haben Sie beispielsweise das Konzept eines Client. Dann PreferredClienttaucht später ein neues Konzept eines auf. Sollte PreferredClienterben Client? Ein bevorzugter Kunde ist doch ein Kunde, oder? Nun, nicht so schnell ... wie Sie sagten, können Objekte ihre Klasse zur Laufzeit nicht ändern. Wie würden Sie die client.makePreferred()Operation modellieren ? Vielleicht liegt die Antwort in der Verwendung von Komposition mit einem fehlenden Konzept, Accountvielleicht?
Plalx

Anstatt verschiedene Arten von ClientKlassen zu haben, gibt es vielleicht nur eine, die das Konzept einer Klasse zusammenfasst, Accountdie eine StandardAccountoder eine sein könnte PreferredAccount...
Plalx

40

Ich habe hier keine zufriedenstellende Antwort gefunden, also habe ich eine neue geschrieben.

Um zu verstehen warum " lieber Komposition der Vererbung“, müssen wir wieder zuerst die Annahme in diesem verkürzten Idiom weggelassen.

Vererbung bietet zwei Vorteile: Subtypisierung und Unterklassifizierung

  1. Subtypisierung bedeutet, einer Typ- (Schnittstellen-) Signatur, dh einer Reihe von APIs, zu entsprechen, und man kann einen Teil der Signatur überschreiben, um einen Subtypisierungspolymorphismus zu erzielen.

  2. Unterklassifizierung bedeutet implizite Wiederverwendung von Methodenimplementierungen.

Die beiden Vorteile bieten zwei verschiedene Verwendungszwecke: subtypisierungsorientiert und Code-Wiederverwendungsorientiert.

Wenn die Wiederverwendung von Code der einzige Zweck ist, kann eine Unterklasse mehr geben, als er benötigt, dh einige öffentliche Methoden der übergeordneten Klasse sind für die untergeordnete Klasse nicht sehr sinnvoll. In diesem Fall wird die Zusammensetzung gefordert , anstatt die Zusammensetzung der Vererbung vorzuziehen . Hier kommt auch der Begriff "ist-ein" gegen "hat-ein" her.

Nur wenn die Subtypisierung beabsichtigt ist, dh die neue Klasse später polymorph zu verwenden, stehen wir vor dem Problem, Vererbung oder Zusammensetzung zu wählen. Dies ist die Annahme, die in der diskutierten verkürzten Sprache weggelassen wird.

Subtyp bedeutet, einer Typensignatur zu entsprechen. Dies bedeutet, dass die Komposition immer nicht weniger APIs des Typs verfügbar machen muss. Jetzt beginnen die Kompromisse:

  1. Die Vererbung bietet eine einfache Wiederverwendung von Code, wenn sie nicht überschrieben wird, während die Komposition jede API neu codieren muss, selbst wenn es sich nur um eine einfache Delegierungsaufgabe handelt.

  2. Die Vererbung bietet eine einfache offene Rekursion über die interne polymorphe Site this, dh das Aufrufen einer überschreibenden Methode (oder sogar eines Typs ) in einer anderen Mitgliedsfunktion, entweder öffentlich oder privat (obwohl davon abgeraten ). Offene Rekursion kann über Komposition simuliert werden , erfordert jedoch zusätzlichen Aufwand und ist möglicherweise nicht immer realisierbar (?). Diese Antwort auf eine doppelte Frage spricht etwas Ähnliches.

  3. Durch die Vererbung werden geschützte Mitglieder freigelegt . Dadurch wird die Kapselung der übergeordneten Klasse unterbrochen, und bei Verwendung durch die Unterklasse wird eine weitere Abhängigkeit zwischen dem untergeordneten Element und seinem übergeordneten Element eingeführt.

  4. Die Zusammensetzung hat den Vorteil einer Umkehrung der Kontrolle, und ihre Abhängigkeit kann dynamisch injiziert werden, wie im Dekorationsmuster und im Proxy-Muster gezeigt .

  5. Die Komposition hat den Vorteil einer kombinatororientierten Programmierung, dh sie funktioniert ähnlich wie das zusammengesetzte Muster .

  6. Die Komposition folgt unmittelbar auf die Programmierung einer Schnittstelle .

  7. Die Zusammensetzung hat den Vorteil einer einfachen Mehrfachvererbung .

In Anbetracht der oben genannten Kompromisse ziehen wir daher die Zusammensetzung der Vererbung vor. Für eng verwandte Klassen, dh wenn die implizite Wiederverwendung von Code wirklich Vorteile bringt oder die magische Kraft der offenen Rekursion gewünscht wird, sollte die Vererbung die Wahl sein.


34

Persönlich habe ich gelernt, Komposition immer der Vererbung vorzuziehen. Es gibt kein programmatisches Problem, das Sie mit der Vererbung lösen können, das Sie mit der Komposition nicht lösen können. In einigen Fällen müssen Sie jedoch möglicherweise Schnittstellen (Java) oder Protokolle (Obj-C) verwenden. Da C ++ so etwas nicht kennt, müssen Sie abstrakte Basisklassen verwenden, was bedeutet, dass Sie die Vererbung in C ++ nicht vollständig entfernen können.

Die Komposition ist oft logischer, bietet eine bessere Abstraktion, eine bessere Kapselung, eine bessere Wiederverwendung von Code (insbesondere in sehr großen Projekten) und ist weniger wahrscheinlich, dass irgendetwas aus der Ferne beschädigt wird, nur weil Sie irgendwo in Ihrem Code eine isolierte Änderung vorgenommen haben. Es macht es auch einfacher, das " Prinzip der Einzelverantwortung " aufrechtzuerhalten , das oft als " Es sollte nie mehr als einen Grund für einen Klassenwechsel geben. " zusammengefasst wird. Dies bedeutet, dass jede Klasse für einen bestimmten Zweck existiert und dies auch sollte haben nur Methoden, die in direktem Zusammenhang mit ihrem Zweck stehen. Ein sehr flacher Vererbungsbaum macht es auch viel einfacher, den Überblick zu behalten, selbst wenn Ihr Projekt sehr groß wird. Viele Leute denken, dass Vererbung unsere repräsentiert reale Weltziemlich gut, aber das ist nicht die Wahrheit. Die reale Welt verwendet viel mehr Komposition als Vererbung. Nahezu jedes Objekt der realen Welt, das Sie in der Hand halten können, wurde aus anderen, kleineren Objekten der realen Welt zusammengesetzt.

Es gibt jedoch Nachteile der Komposition. Wenn Sie die Vererbung insgesamt überspringen und sich nur auf die Komposition konzentrieren, werden Sie feststellen, dass Sie häufig einige zusätzliche Codezeilen schreiben müssen, die bei Verwendung der Vererbung nicht erforderlich waren. Manchmal müssen Sie sich auch wiederholen, was gegen das DRY-Prinzip verstößt(DRY = Wiederholen Sie sich nicht). Außerdem erfordert die Komposition häufig eine Delegierung, und eine Methode ruft nur eine andere Methode eines anderen Objekts auf, ohne dass dieser Aufruf von einem anderen Code umgeben ist. Solche "doppelten Methodenaufrufe" (die sich leicht auf dreifache oder vierfache Methodenaufrufe erstrecken können und sogar noch weiter) haben eine viel schlechtere Leistung als die Vererbung, bei der Sie einfach eine Methode Ihres Elternteils erben. Das Aufrufen einer geerbten Methode kann genauso schnell sein wie das Aufrufen einer nicht geerbten Methode, oder es kann etwas langsamer sein, ist jedoch normalerweise immer noch schneller als zwei aufeinanderfolgende Methodenaufrufe.

Möglicherweise haben Sie bemerkt, dass die meisten OO-Sprachen keine Mehrfachvererbung zulassen. Es gibt zwar einige Fälle, in denen Mehrfachvererbung Ihnen wirklich etwas kaufen kann, aber dies sind eher Ausnahmen als die Regel. Immer wenn Sie in eine Situation geraten, in der Sie der Meinung sind, dass "Mehrfachvererbung eine wirklich coole Funktion zur Lösung dieses Problems wäre", befinden Sie sich normalerweise an einem Punkt, an dem Sie die Vererbung insgesamt überdenken sollten, da möglicherweise sogar einige zusätzliche Codezeilen erforderlich sind Eine auf Komposition basierende Lösung wird sich normalerweise als viel eleganter, flexibler und zukunftssicherer herausstellen.

Vererbung ist wirklich ein cooles Feature, aber ich fürchte, es wurde in den letzten Jahren überbeansprucht. Die Menschen behandelten die Vererbung als den einen Hammer, der alles festnageln kann, unabhängig davon, ob es sich tatsächlich um einen Nagel, eine Schraube oder etwas völlig anderes handelt.


"Viele Leute denken, dass Vererbung unsere reale Welt ziemlich gut repräsentiert, aber das ist nicht die Wahrheit." Soviel dazu! Im Gegensatz zu so ziemlich allen Programmier-Tutorials der Welt ist es auf lange Sicht wahrscheinlich eine schlechte Idee, reale Objekte als Vererbungsketten zu modellieren. Sie sollten Vererbung nur verwenden, wenn es eine unglaublich offensichtliche, angeborene, einfache Beziehung gibt. Wie TextFileist ein File.
Neonblitzer

25

Meine allgemeine Faustregel: Überlegen Sie vor der Verwendung der Vererbung, ob die Komposition sinnvoller ist.

Grund: Unterklassen bedeuten normalerweise mehr Komplexität und Verbundenheit, dh es ist schwieriger, Fehler zu ändern, zu warten und zu skalieren, ohne Fehler zu machen.

Eine viel vollständigere und konkretere Antwort von Tim Boudreau von Sun:

Häufige Probleme bei der Verwendung der Vererbung, wie ich sie sehe, sind:

  • Unschuldige Handlungen können zu unerwarteten Ergebnissen führen. Das klassische Beispiel hierfür sind Aufrufe überschreibbarer Methoden vom Superklassenkonstruktor, bevor die Instanzfelder der Unterklassen initialisiert wurden. In einer perfekten Welt würde das niemals jemand tun. Dies ist keine perfekte Welt.
  • Es bietet perverse Versuchungen für Unterklassen, Annahmen über die Reihenfolge der Methodenaufrufe und dergleichen zu treffen - solche Annahmen sind in der Regel nicht stabil, wenn sich die Oberklasse im Laufe der Zeit weiterentwickelt. Siehe auch meine Analogie zwischen Toaster und Kaffeekanne .
  • Klassen werden schwerer - Sie wissen nicht unbedingt, welche Arbeit Ihre Superklasse in ihrem Konstruktor leistet oder wie viel Speicher sie verwenden wird. Das Konstruieren eines unschuldigen, leichtgewichtigen Objekts kann also weitaus teurer sein als Sie denken, und dies kann sich im Laufe der Zeit ändern, wenn sich die Oberklasse weiterentwickelt
  • Es fördert eine Explosion von Unterklassen . Das Laden von Klassen kostet Zeit, mehr Klassen kosten Speicher. Dies ist möglicherweise kein Problem, bis Sie sich mit einer App auf der Skala von NetBeans befassen. Dort hatten wir jedoch echte Probleme mit beispielsweise langsamen Menüs, da die erste Anzeige eines Menüs ein massives Laden von Klassen auslöste. Wir haben dies behoben, indem wir auf eine deklarativere Syntax und andere Techniken umgestiegen sind, aber die Behebung hat auch Zeit gekostet.
  • Es macht es schwieriger, Dinge später zu ändern - wenn Sie eine Klasse öffentlich gemacht haben, wird das Austauschen der Oberklasse Unterklassen unterbrechen - es ist eine Wahl, mit der Sie verheiratet sind, sobald Sie den Code veröffentlicht haben. Wenn Sie also die reale Funktionalität Ihrer Superklasse nicht ändern, haben Sie viel mehr Freiheit, Dinge später zu ändern, wenn Sie sie verwenden, anstatt das zu erweitern, was Sie benötigen. Nehmen Sie zum Beispiel die Unterklasse JPanel - dies ist normalerweise falsch. und wenn die Unterklasse irgendwo öffentlich ist, haben Sie nie die Möglichkeit, diese Entscheidung zu überdenken. Wenn auf den Zugriff als JComponent getThePanel () zugegriffen wird, können Sie dies dennoch tun (Hinweis: Stellen Sie Modelle für die darin enthaltenen Komponenten als API bereit).
  • Objekthierarchien skalieren nicht (oder es ist viel schwieriger, sie später zu skalieren, als vorauszuplanen) - dies ist das klassische Problem "zu viele Ebenen". Ich werde weiter unten darauf eingehen und wie das AskTheOracle-Muster es lösen kann (obwohl es OOP-Puristen beleidigen kann).

...

Ich nehme an, was zu tun ist, wenn Sie eine Vererbung zulassen, die Sie mit einem Körnchen Salz nehmen können:

  • Stellen Sie niemals Felder außer Konstanten zur Verfügung
  • Die Methoden müssen entweder abstrakt oder endgültig sein
  • Rufen Sie keine Methoden vom Superklassenkonstruktor auf

...

All dies gilt weniger für kleine als für große Projekte und weniger für Privatunterricht als für öffentliche


25

Warum Komposition gegenüber Vererbung bevorzugen?

Siehe andere Antworten.

Wann können Sie die Vererbung verwenden?

Es wird oft gesagt, dass eine Klasse eine Klasse Barerben kann, Foowenn der folgende Satz wahr ist:

  1. Eine Bar ist ein Foo

Leider ist der obige Test allein nicht zuverlässig. Verwenden Sie stattdessen Folgendes:

  1. Eine Bar ist ein Foo, UND
  2. Bars können alles, was Foos können.

Der erste Test stellt sicher , dass alle Getter von FooSinne in Bar(= shared Eigenschaften), während der zweite Test stellt sicher , dass alle Setter von FooSinne in Bar(= shared - Funktionalität).

Beispiel 1: Hund -> Tier

Ein Hund ist ein Tier UND Hunde können alles, was Tiere können (wie Atmen, Sterben usw.). Daher ist die Klasse Dog kann die Klasse erben Animal.

Beispiel 2: Kreis - / -> Ellipse

Ein Kreis ist eine Ellipse, ABER Kreise können nicht alles, was Ellipsen können. Zum Beispiel können sich Kreise nicht dehnen, Ellipsen dagegen. Daher kann die Klasse die Klasse Circle nicht erben Ellipse.

Dies wird als Circle-Ellipse-Problem bezeichnet , was wirklich kein Problem ist, sondern nur ein klarer Beweis dafür, dass der erste Test allein nicht ausreicht, um zu dem Schluss zu kommen, dass eine Vererbung möglich ist. Insbesondere dieses Beispiel Highlights , die Klassen abgeleitet sollte erweitern die Funktionalität von Basisklassen, nie beschränken sie. Andernfalls könnte die Basisklasse nicht polymorph verwendet werden.

Wann sollten Sie die Vererbung verwenden?

Auch wenn Sie Vererbung verwenden können , heißt das nicht, dass Sie dies tun sollten : Die Verwendung von Komposition ist immer eine Option. Vererbung ist ein leistungsstarkes Tool, das die implizite Wiederverwendung von Code und den dynamischen Versand ermöglicht. Es weist jedoch einige Nachteile auf, weshalb die Komposition häufig bevorzugt wird. Die Kompromisse zwischen Vererbung und Zusammensetzung sind nicht offensichtlich und lassen sich meiner Meinung nach am besten in der Antwort von lcn erklären .

Als Faustregel tendiere ich dazu, die Vererbung der Zusammensetzung vorzuziehen, wenn eine sehr häufige polymorphe Verwendung zu erwarten ist. In diesem Fall kann die Leistungsfähigkeit des dynamischen Versands zu einer viel lesbareren und eleganteren API führen. Wenn Sie beispielsweise eine polymorphe Klasse Widgetin GUI-Frameworks oder eine polymorphe Klasse Nodein XML-Bibliotheken haben, können Sie eine API verwenden, die viel lesbarer und intuitiver zu verwenden ist als bei einer rein kompositionsbasierten Lösung.

Liskov-Substitutionsprinzip

Damit Sie wissen, wird das Liskov-Substitutionsprinzip als eine andere Methode bezeichnet, mit der festgestellt wird, ob eine Vererbung möglich ist :

Funktionen, die Zeiger oder Verweise auf Basisklassen verwenden, müssen Objekte abgeleiteter Klassen verwenden können, ohne es zu wissen

Im Wesentlichen bedeutet dies, dass Vererbung möglich ist, wenn die Basisklasse polymorph verwendet werden kann, was meiner Meinung nach unserem Test entspricht: "Ein Balken ist ein Foo und Balken können alles, was Foos können".


Die Szenarien mit Kreisellipse und quadratischem Rechteck sind schlechte Beispiele. Unterklassen sind ausnahmslos komplexer als ihre Oberklasse, daher ist das Problem erfunden. Dieses Problem wird durch Invertieren der Beziehung gelöst. Eine Ellipse ergibt sich aus einem Kreis und ein Rechteck aus einem Quadrat. Es ist extrem dumm, in diesen Szenarien Komposition zu verwenden.
Fuzzy Logic

@FuzzyLogic Einverstanden, aber tatsächlich befürwortet mein Beitrag in diesem Fall niemals die Verwendung von Komposition. Ich habe nur gesagt, dass das Kreisellipsenproblem ein gutes Beispiel dafür ist, warum "is-a" allein kein guter Test ist, um zu dem Schluss zu kommen, dass Circle von Ellipse abgeleitet werden sollte. Sobald wir zu dem Schluss kommen, dass Circle aufgrund einer Verletzung von LSP eigentlich nicht von Ellipse abgeleitet werden sollte, können Sie die Beziehung invertieren, die Komposition verwenden, Vorlagenklassen verwenden oder ein komplexeres Design mit zusätzlichen Klassen oder Hilfsfunktionen verwenden. etc ... und die Entscheidung sollte natürlich von Fall zu Fall getroffen werden.
Boris Dalstein

1
@FuzzyLogic Und wenn Sie neugierig sind, was ich für den speziellen Fall von Circle-Ellipse befürworten würde: Ich würde befürworten, die Circle-Klasse nicht zu implementieren. Das Problem beim Invertieren der Beziehung ist, dass sie auch gegen LSP verstößt: Stellen Sie sich die Funktion vor computeArea(Circle* c) { return pi * square(c->radius()); }. Es ist offensichtlich kaputt, wenn eine Ellipse passiert wird (was bedeutet Radius () überhaupt?). Eine Ellipse ist kein Kreis und sollte daher nicht vom Kreis abgeleitet werden.
Boris Dalstein

computeArea(Circle *c) { return pi * width * height / 4.0; }Jetzt ist es generisch.
Fuzzy Logic

2
@FuzzyLogic Ich bin anderer Meinung: Sie erkennen, dass dies bedeutet, dass der Klassenkreis die Existenz der abgeleiteten Klasse Ellipse vorweggenommen und daher bereitgestellt hat width()und height()? Was ist, wenn ein Bibliotheksbenutzer jetzt beschließt, eine andere Klasse namens "EggShape" zu erstellen? Sollte es auch von "Circle" abgeleitet sein? Natürlich nicht. Eine Eiform ist kein Kreis, und eine Ellipse ist auch kein Kreis, daher sollte keine von Circle abgeleitet werden, da sie LSP bricht. Methoden, die Operationen für eine Circle * -Klasse ausführen, machen starke Annahmen darüber, was ein Kreis ist, und das Brechen dieser Annahmen führt mit ziemlicher Sicherheit zu Fehlern.
Boris Dalstein

19

Die Vererbung ist sehr mächtig, aber Sie können sie nicht erzwingen (siehe: das Kreisellipsenproblem ). Wenn Sie sich einer echten Subtyp-Beziehung nicht wirklich sicher sein können, ist es am besten, mit der Komposition zu beginnen.


15

Vererbung schafft eine starke Beziehung zwischen einer Unterklasse und einer Superklasse; Die Unterklasse muss die Implementierungsdetails der Superklasse kennen. Das Erstellen der Superklasse ist viel schwieriger, wenn Sie darüber nachdenken müssen, wie sie erweitert werden kann. Sie müssen Klasseninvarianten sorgfältig dokumentieren und angeben, welche anderen Methoden überschreibbare Methoden intern verwenden.

Vererbung ist manchmal nützlich, wenn die Hierarchie wirklich eine Beziehung darstellt. Es bezieht sich auf das Open-Closed-Prinzip, das besagt, dass Klassen zur Änderung geschlossen, aber für Erweiterungen offen sein sollten. Auf diese Weise können Sie Polymorphismus haben; um eine generische Methode zu haben, die sich mit dem Supertyp und seinen Methoden befasst, aber über den dynamischen Versand wird die Methode der Unterklasse aufgerufen. Dies ist flexibel und hilft bei der Erstellung von Indirektionen, die für Software unerlässlich sind (um weniger über Implementierungsdetails zu erfahren).

Vererbung wird jedoch leicht überbeansprucht und schafft zusätzliche Komplexität mit starken Abhängigkeiten zwischen Klassen. Auch das Verstehen, was während der Ausführung eines Programms passiert, wird aufgrund von Ebenen und der dynamischen Auswahl von Methodenaufrufen ziemlich schwierig.

Ich würde vorschlagen, das Komponieren als Standard zu verwenden. Es ist modularer und bietet den Vorteil einer späten Bindung (Sie können die Komponente dynamisch ändern). Es ist auch einfacher, die Dinge separat zu testen. Und wenn Sie eine Methode aus einer Klasse verwenden müssen, müssen Sie keine bestimmte Form haben (Liskov-Substitutionsprinzip).


3
Es ist erwähnenswert, dass Vererbung nicht der einzige Weg ist, um Polymorphismus zu erreichen. Das Dekorationsmuster bietet das Erscheinungsbild von Polymorphismus durch Komposition.
BitMask777

1
@ BitMask777: Subtyp-Polymorphismus ist nur eine Art von Polymorphismus, eine andere wäre parametrischer Polymorphismus, dafür benötigen Sie keine Vererbung. Noch wichtiger: Wenn man von Vererbung spricht, bedeutet man Klassenvererbung; .ie Sie können Subtyp-Polymorphismus haben, indem Sie eine gemeinsame Schnittstelle für mehrere Klassen haben, und Sie bekommen nicht die Probleme der Vererbung.
Egaga

2
@engaga: Ich habe Ihren Kommentar Inheritance is sometimes useful... That way you can have polymorphismso interpretiert , dass er die Konzepte von Vererbung und Polymorphismus fest verknüpft (Subtypisierung wird im Kontext angenommen). Mein Kommentar sollte darauf hinweisen, was Sie in Ihrem Kommentar klarstellen: Diese Vererbung ist nicht die einzige Möglichkeit, Polymorphismus zu implementieren, und ist in der Tat nicht unbedingt der entscheidende Faktor bei der Entscheidung zwischen Zusammensetzung und Vererbung.
BitMask777

15

Angenommen, ein Flugzeug besteht nur aus zwei Teilen: einem Motor und Flügeln.
Dann gibt es zwei Möglichkeiten, eine Flugzeugklasse zu entwerfen.

Class Aircraft extends Engine{
  var wings;
}

Jetzt kann Ihr Flugzeug mit festen Flügeln beginnen
und diese im laufenden Betrieb in Drehflügel verwandeln. Es ist im Wesentlichen
ein Motor mit Flügeln. Aber was ist, wenn ich mich ändern wollte?
den Motor auch im laufenden Betrieb ?

Entweder macht die Basisklasse Engineeinen Mutator verfügbar, um seine
Eigenschaften zu ändern , oder ich gestalte ihn neu Aircraftals:

Class Aircraft {
  var wings;
  var engine;
}

Jetzt kann ich meinen Motor auch im laufenden Betrieb austauschen.


Ihr Beitrag bringt einen Punkt auf den Punkt, den ich vorher nicht in Betracht gezogen hatte - um eine Analogie mechanischer Objekte mit mehreren Teilen fortzusetzen, auf einer Art Schusswaffe, gibt es im Allgemeinen einen Teil, der mit einer Seriennummer gekennzeichnet ist, deren Seriennummer als solche gilt das der Waffe als Ganzes (für eine Pistole wäre es normalerweise der Rahmen). Man kann alle anderen Teile ersetzen und immer noch die gleiche Waffe haben, aber wenn der Rahmen reißt und ersetzt werden muss, wäre das Ergebnis der Montage eines neuen Rahmens mit allen anderen Teilen der Originalwaffe eine neue Waffe. Beachten Sie, dass ...
Supercat

... die Tatsache, dass auf mehreren Teilen einer Waffe möglicherweise Seriennummern angegeben sind, bedeutet nicht, dass eine Waffe mehrere Identitäten haben kann. Nur die Seriennummer auf dem Rahmen kennzeichnet die Waffe. Die Seriennummer eines anderen Teils gibt an, mit welcher Pistole diese Teile zusammengebaut werden sollen. Dies ist möglicherweise nicht die Pistole, an der sie zu einem bestimmten Zeitpunkt montiert sind.
Supercat


7

Wenn Sie die API der Basisklasse "kopieren" / verfügbar machen möchten, verwenden Sie die Vererbung. Wenn Sie nur Funktionen "kopieren" möchten, verwenden Sie die Delegierung.

Ein Beispiel hierfür: Sie möchten einen Stapel aus einer Liste erstellen. Stack hat nur Pop, Push und Peek. Sie sollten die Vererbung nicht verwenden, da Sie keine Push_back-, Push_front-, removeAt- und andere Funktionen in einem Stack wünschen.


7

Diese beiden Wege können gut zusammenleben und sich gegenseitig unterstützen.

Composition spielt es nur modular ab: Sie erstellen eine Schnittstelle ähnlich der übergeordneten Klasse, erstellen ein neues Objekt und delegieren Aufrufe an diese. Wenn diese Objekte sich nicht kennen müssen, ist die Komposition recht sicher und einfach zu verwenden. Hier gibt es so viele Möglichkeiten.

Wenn die übergeordnete Klasse jedoch aus irgendeinem Grund auf Funktionen zugreifen muss, die von der "untergeordneten Klasse" für unerfahrene Programmierer bereitgestellt werden, scheint dies ein großartiger Ort für die Verwendung der Vererbung zu sein. Die übergeordnete Klasse kann einfach ihre eigene Zusammenfassung "foo ()" nennen, die von der Unterklasse überschrieben wird, und dann der abstrakten Basis den Wert geben.

Es sieht nach einer guten Idee aus, aber in vielen Fällen ist es besser, der Klasse einfach ein Objekt zu geben, das foo () implementiert (oder sogar den Wert für foo () manuell festzulegen), als die neue Klasse von einer Basisklasse zu erben, die dies erfordert die anzugebende Funktion foo ().

Warum?

Weil Vererbung eine schlechte Art ist, Informationen zu verschieben .

Die Komposition hat hier einen echten Vorteil: Die Beziehung kann umgekehrt werden: Die "übergeordnete Klasse" oder der "abstrakte Arbeiter" kann bestimmte "untergeordnete" Objekte aggregieren, die eine bestimmte Schnittstelle implementieren. + Jedes untergeordnete Objekt kann in einem anderen übergeordneten Typ festgelegt werden, der dies akzeptiert Es ist Typ . Und es kann eine beliebige Anzahl von Objekten geben, zum Beispiel könnte MergeSort oder QuickSort eine beliebige Liste von Objekten sortieren, die eine abstrakte Compare-Schnittstelle implementieren. Oder anders ausgedrückt: Jede Gruppe von Objekten, die "foo ()" implementieren, und jede andere Gruppe von Objekten, die Objekte mit "foo ()" verwenden können, können zusammen spielen.

Ich kann mir drei echte Gründe für die Verwendung der Vererbung vorstellen:

  1. Sie haben viele Klassen mit derselben Oberfläche und möchten Zeit beim Schreiben sparen
  2. Sie müssen für jedes Objekt dieselbe Basisklasse verwenden
  3. Sie müssen die privaten Variablen ändern, die auf keinen Fall öffentlich sein dürfen

Wenn dies zutrifft, muss wahrscheinlich die Vererbung verwendet werden.

Die Verwendung von Grund 1 ist nicht schlecht. Es ist sehr gut, eine solide Schnittstelle für Ihre Objekte zu haben. Dies kann problemlos mit Komposition oder mit Vererbung erfolgen - wenn diese Schnittstelle einfach ist und sich nicht ändert. Normalerweise ist die Vererbung hier sehr effektiv.

Wenn der Grund Nummer 2 ist, wird es etwas schwierig. Müssen Sie wirklich nur dieselbe Basisklasse verwenden? Im Allgemeinen ist es nicht gut genug, nur dieselbe Basisklasse zu verwenden, aber es kann eine Anforderung Ihres Frameworks sein, eine Entwurfsüberlegung, die nicht vermieden werden kann.

Wenn Sie jedoch die privaten Variablen verwenden möchten, Fall 3, sind Sie möglicherweise in Schwierigkeiten. Wenn Sie globale Variablen als unsicher betrachten, sollten Sie die Verwendung der Vererbung in Betracht ziehen, um Zugriff auf private Variablen zu erhalten, die ebenfalls unsicher sind . Wohlgemerkt, globale Variablen sind nicht alles so schlecht - Datenbanken sind im Wesentlichen große Mengen globaler Variablen. Aber wenn Sie damit umgehen können, ist es ganz gut.


7

Um diese Frage für neuere Programmierer aus einer anderen Perspektive zu beantworten:

Vererbung wird oft früh gelehrt, wenn wir objektorientiertes Programmieren lernen. Daher wird es als einfache Lösung für ein häufiges Problem angesehen.

Ich habe drei Klassen, die alle einige gemeinsame Funktionen benötigen. Wenn ich also eine Basisklasse schreibe und sie alle davon erben lasse, haben sie alle diese Funktionalität und ich muss sie nur an einer Stelle warten.

Es klingt großartig, aber in der Praxis funktioniert es aus einem von mehreren Gründen fast nie.

  • Wir stellen fest, dass es einige andere Funktionen gibt, die unsere Klassen haben sollen. Wenn die Art und Weise, wie wir Klassen Funktionen hinzufügen, durch Vererbung erfolgt, müssen wir uns entscheiden: Fügen wir sie der vorhandenen Basisklasse hinzu, obwohl nicht jede Klasse, die von ihr erbt, diese Funktionalität benötigt? Erstellen wir eine weitere Basisklasse? Aber was ist mit Klassen, die bereits von der anderen Basisklasse erben?
  • Wir stellen fest, dass sich die Basisklasse für nur eine der Klassen, die von unserer Basisklasse erbt, etwas anders verhält. Jetzt gehen wir zurück und basteln an unserer Basisklasse und fügen möglicherweise einige virtuelle Methoden oder noch schlimmer einen Code hinzu, der besagt: "Wenn ich Typ A geerbt habe, tun Sie dies, aber wenn ich Typ B geerbt habe, tun Sie das . " Das ist aus vielen Gründen schlecht. Zum einen ändern wir jedes Mal, wenn wir die Basisklasse ändern, effektiv jede geerbte Klasse. Wir ändern also wirklich die Klassen A, B, C und D, weil wir in Klasse A ein etwas anderes Verhalten benötigen. So vorsichtig wir auch sind, wir könnten eine dieser Klassen aus Gründen brechen, die nichts damit zu tun haben Klassen.
  • Wir wissen vielleicht, warum wir beschlossen haben, alle diese Klassen voneinander zu erben, aber für jemanden, der unseren Code pflegen muss, ist dies möglicherweise (wahrscheinlich auch nicht) sinnvoll. Wir könnten sie zu einer schwierigen Entscheidung zwingen - mache ich etwas wirklich Hässliches und Unordentliches, um die Änderung vorzunehmen, die ich brauche (siehe den vorherigen Punkt), oder schreibe ich einfach ein paar davon neu.

Am Ende binden wir unseren Code in einige schwierige Knoten und profitieren überhaupt nicht davon, außer dass wir sagen können: "Cool, ich habe etwas über Vererbung gelernt und jetzt habe ich ihn verwendet." Das soll nicht herablassend sein, weil wir es alle getan haben. Aber wir alle haben es getan, weil uns niemand gesagt hat, dass wir es nicht tun sollen.

Sobald mir jemand erklärte, "Komposition gegenüber Vererbung bevorzugen", dachte ich jedes Mal zurück, wenn ich versuchte, Funktionen mithilfe von Vererbung zwischen Klassen zu teilen, und stellte fest, dass dies die meiste Zeit nicht wirklich gut funktionierte.

Das Gegenmittel ist das Prinzip der Einzelverantwortung . Betrachten Sie es als Einschränkung. Meine Klasse muss eines tun. Ich muss in der Lage sein, meiner Klasse einen Namen zu geben, der irgendwie beschreibt, was sie tut. (Es gibt Ausnahmen zu allem, aber absolute Regeln sind manchmal besser, wenn wir lernen.) Daraus folgt, dass ich keine Basisklasse namens schreiben kann ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses. Welche bestimmte Funktionalität ich auch benötige, muss in einer eigenen Klasse sein, und dann können andere Klassen, die diese Funktionalität benötigen, von dieser Klasse abhängen und nicht von ihr erben.

Auf die Gefahr einer zu starken Vereinfachung hin besteht diese Komposition darin, mehrere Klassen zusammenzustellen, um zusammenzuarbeiten. Und sobald wir diese Gewohnheit entwickelt haben, stellen wir fest, dass sie viel flexibler, wartbarer und testbarer ist als die Verwendung von Vererbung.


Dass Klassen nicht mehrere Basisklassen verwenden können, ist keine schlechte Reflexion über die Vererbung, sondern eine schlechte Reflexion über die mangelnden Fähigkeiten einer bestimmten Sprache.
iPherian

In der Zeit seit dem Schreiben dieser Antwort habe ich diesen Beitrag von "Onkel Bob" gelesen, der diesen Mangel an Fähigkeiten behebt. Ich habe noch nie eine Sprache verwendet, die Mehrfachvererbung zulässt. Aber im Rückblick ist die Frage mit "sprachunabhängig" gekennzeichnet und meine Antwort geht von C # aus. Ich muss meinen Horizont erweitern.
Scott Hannen

6

Abgesehen von a / hat eine Überlegung, muss man auch die "Tiefe" der Vererbung berücksichtigen, die Ihr Objekt durchlaufen muss. Alles, was über fünf oder sechs Vererbungsebenen hinausgeht, kann zu unerwarteten Casting- und Box- / Unboxing-Problemen führen. In diesen Fällen ist es möglicherweise ratsam, stattdessen Ihr Objekt zu erstellen.


6

Wenn Sie eine Beziehung zwischen zwei Klassen haben (Beispiel: Hund ist ein Hund), streben Sie eine Vererbung an.

Wenn Sie andererseits eine oder eine Adjektivbeziehung zwischen zwei Klassen haben (Schüler hat Kurse) oder (Lehrerkurse), haben Sie Komposition gewählt.


Sie sagten Vererbung und Vererbung. meinst du nicht Vererbung und Zusammensetzung?
TrevorKirkby

Nein, tust du nicht. Sie können genauso gut eine Hundeschnittstelle definieren und von jedem Hund implementieren lassen, sodass Sie am Ende mehr SOLID-Code erhalten.
Markus

5

Ein einfacher Weg, dies zu verstehen, wäre, dass die Vererbung verwendet werden sollte, wenn ein Objekt Ihrer Klasse dieselbe Schnittstelle wie seine übergeordnete Klasse haben muss, damit es dadurch als Objekt der übergeordneten Klasse behandelt werden kann (Upcasting). . Darüber hinaus würden Funktionsaufrufe für ein abgeleitetes Klassenobjekt überall im Code gleich bleiben, aber die spezifische aufzurufende Methode würde zur Laufzeit bestimmt (dh die Implementierung auf niedriger Ebene unterscheidet sich, die Schnittstelle auf hoher Ebene bleibt gleich).

Die Komposition sollte verwendet werden, wenn die neue Klasse nicht dieselbe Schnittstelle haben muss, dh wenn Sie bestimmte Aspekte der Klassenimplementierung verbergen möchten, über die der Benutzer dieser Klasse nichts wissen muss. Die Komposition unterstützt also eher die Kapselung (dh das Verbergen der Implementierung), während die Vererbung die Abstraktion unterstützen soll (dh eine vereinfachte Darstellung von etwas, in diesem Fall dieselbe Schnittstelle für eine Reihe von Typen mit unterschiedlichen Interna).


+1 für die Erwähnung der Schnittstelle. Ich verwende diesen Ansatz häufig, um vorhandene Klassen auszublenden und meine neue Klasse ordnungsgemäß testbar zu machen, indem ich das für die Komposition verwendete Objekt verspotte. Dies erfordert, dass der Eigentümer des neuen Objekts ihm stattdessen die übergeordnete Kandidatenklasse übergibt.
Kell


4

Ich stimme @Pavel zu, wenn er sagt, es gibt Orte für Komposition und Orte für Vererbung.

Ich denke, Vererbung sollte verwendet werden, wenn Ihre Antwort eine dieser Fragen bestätigt.

  • Ist Ihre Klasse Teil einer Struktur, die vom Polymorphismus profitiert? Wenn Sie beispielsweise eine Shape-Klasse hatten, die eine Methode namens draw () deklariert, müssen Circle- und Square-Klassen eindeutig Unterklassen von Shape sein, damit ihre Client-Klassen von Shape und nicht von bestimmten Unterklassen abhängen.
  • Muss Ihre Klasse alle in einer anderen Klasse definierten Interaktionen auf hoher Ebene wiederverwenden? Das Entwurfsmuster der Vorlagenmethode wäre ohne Vererbung nicht zu implementieren. Ich glaube, dass alle erweiterbaren Frameworks dieses Muster verwenden.

Wenn Sie jedoch nur die Wiederverwendung von Code beabsichtigen, ist die Komposition höchstwahrscheinlich die bessere Wahl für das Design.


4

Vererbung ist ein sehr mächtiger Machanismus für die Wiederverwendung von Code. Muss aber richtig eingesetzt werden. Ich würde sagen, dass die Vererbung korrekt verwendet wird, wenn die Unterklasse auch ein Subtyp der übergeordneten Klasse ist. Wie oben erwähnt, ist das Liskov-Substitutionsprinzip hier der entscheidende Punkt.

Unterklasse ist nicht dasselbe wie Untertyp. Sie können Unterklassen erstellen, die keine Untertypen sind (und in diesem Fall sollten Sie Komposition verwenden). Um zu verstehen, was ein Subtyp ist, erklären wir zunächst, was ein Typ ist.

Wenn wir sagen, dass die Zahl 5 vom Typ Integer ist, geben wir an, dass 5 zu einer Reihe möglicher Werte gehört (siehe als Beispiel die möglichen Werte für die primitiven Java-Typen). Wir geben auch an, dass es eine gültige Reihe von Methoden gibt, die ich für den Wert wie Addition und Subtraktion ausführen kann. Und schließlich geben wir an, dass es eine Reihe von Eigenschaften gibt, die immer erfüllt sind. Wenn ich beispielsweise die Werte 3 und 5 addiere, erhalte ich 8.

Um ein weiteres Beispiel zu nennen, denken Sie an die abstrakten Datentypen, Satz von Ganzzahlen und Liste von Ganzzahlen. Die Werte, die sie enthalten können, sind auf Ganzzahlen beschränkt. Beide unterstützen eine Reihe von Methoden wie add (newValue) und size (). Und beide haben unterschiedliche Eigenschaften (klasseninvariant). Sets erlaubt keine Duplikate, während List Duplikate zulässt (natürlich gibt es andere Eigenschaften, die beide erfüllen).

Der Subtyp ist auch ein Typ, der eine Beziehung zu einem anderen Typ hat, der als übergeordneter Typ (oder Supertyp) bezeichnet wird. Der Subtyp muss den Merkmalen (Werten, Methoden und Eigenschaften) des übergeordneten Typs entsprechen. Die Beziehung bedeutet, dass der Supertyp in jedem Kontext, in dem er erwartet wird, durch einen Subtyp ersetzt werden kann, ohne das Verhalten der Ausführung zu beeinflussen. Schauen wir uns einen Code an, um zu veranschaulichen, was ich sage. Angenommen, ich schreibe eine Liste von ganzen Zahlen (in einer Art Pseudosprache):

class List {
  data = new Array();

  Integer size() {
    return data.length;
  }

  add(Integer anInteger) {
    data[data.length] = anInteger;
  }
}

Dann schreibe ich die Menge der ganzen Zahlen als Unterklasse der Liste der ganzen Zahlen:

class Set, inheriting from: List {
  add(Integer anInteger) {
     if (data.notContains(anInteger)) {
       super.add(anInteger);
     }
  }
}

Unsere Set of Integers-Klasse ist eine Unterklasse von List of Integers, jedoch kein Subtyp, da sie nicht alle Funktionen der List-Klasse erfüllt. Die Werte und die Signatur der Methoden sind erfüllt, die Eigenschaften jedoch nicht. Das Verhalten der Methode add (Integer) wurde deutlich geändert, wobei die Eigenschaften des übergeordneten Typs nicht beibehalten wurden. Denken Sie aus der Sicht des Kunden Ihrer Klassen. Sie erhalten möglicherweise eine Reihe von Ganzzahlen, für die eine Liste von Ganzzahlen erwartet wird. Der Client möchte möglicherweise einen Wert hinzufügen und diesen Wert zur Liste hinzufügen, auch wenn dieser Wert bereits in der Liste vorhanden ist. Aber sie wird dieses Verhalten nicht bekommen, wenn der Wert existiert. Eine große Überraschung für sie!

Dies ist ein klassisches Beispiel für eine missbräuchliche Verwendung der Vererbung. Verwenden Sie in diesem Fall die Komposition.

(Ein Fragment aus: Vererbung richtig verwenden ).


3

Eine Faustregel, die ich gehört habe, ist, dass Vererbung verwendet werden sollte, wenn es sich um eine "is-a" -Beziehung handelt, und Zusammensetzung, wenn es sich um ein "has-a" handelt. Trotzdem denke ich, dass Sie sich immer zur Komposition neigen sollten, da dies viel Komplexität beseitigt.


2

Zusammensetzung v / s Vererbung ist ein weites Thema. Es gibt keine wirkliche Antwort auf das, was besser ist, da ich denke, dass alles vom Design des Systems abhängt.

Im Allgemeinen bietet die Art der Beziehung zwischen Objekten bessere Informationen, um eines davon auszuwählen.

Wenn der Beziehungstyp "IS-A" -Relation ist, ist Vererbung ein besserer Ansatz. Andernfalls ist der Beziehungstyp "HAS-A" -Relation, dann nähert sich die Zusammensetzung besser an.

Es hängt völlig von der Entitätsbeziehung ab.


2

Obwohl Zusammensetzung bevorzugt ist, würde Ich mag Profis von hervorzuheben Vererbung und Nachteile der Zusammensetzung .

Vorteile der Vererbung:

  1. Es stellt eine logische " IS A" -Beziehung her. Wenn PKW und LKW zwei Fahrzeugtypen (Basisklasse) sind, ist die untergeordnete Klasse eine Basisklasse.

    dh

    Auto ist ein Fahrzeug

    LKW ist ein Fahrzeug

  2. Mit der Vererbung können Sie eine Funktion definieren / ändern / erweitern

    1. Die Basisklasse bietet keine Implementierung und die Unterklasse muss die vollständige Methode (Zusammenfassung) überschreiben => Sie können einen Vertrag implementieren
    2. Die Basisklasse bietet die Standardimplementierung und die Unterklasse kann das Verhalten ändern => Sie können den Vertrag neu definieren
    3. Die Unterklasse fügt der Implementierung der Basisklasse eine Erweiterung hinzu, indem sie super.methodName () als erste Anweisung aufruft => Sie können einen Vertrag verlängern
    4. Die Basisklasse definiert die Struktur des Algorithmus und die Unterklasse überschreibt einen Teil des Algorithmus => Sie können Template_method implementieren, ohne das Skelett der Basisklasse zu ändern

Nachteile der Zusammensetzung:

  1. Bei der Vererbung kann die Unterklasse die Basisklassenmethode direkt aufrufen, obwohl sie aufgrund der IS A- Beziehung keine Basisklassenmethode implementiert . Wenn Sie Komposition verwenden, müssen Sie Methoden in der Containerklasse hinzufügen, um die enthaltene Klassen-API verfügbar zu machen

zB Wenn das Auto ein Fahrzeug enthält und Sie den Preis des Autos erhalten müssen , der in Fahrzeug definiert wurde, lautet Ihr Code wie folgt

class Vehicle{
     protected double getPrice(){
          // return price
     }
} 

class Car{
     Vehicle vehicle;
     protected double getPrice(){
          return vehicle.getPrice();
     }
} 

Ich denke, es beantwortet die Frage nicht
Almanegra

Sie können sich die OP-Frage noch einmal ansehen. Ich habe angesprochen: Welche Kompromisse gibt es für jeden Ansatz?
Ravindra Babu

Wie Sie bereits erwähnt haben, sprechen Sie nur über "Vor- und Nachteile der Komposition" und nicht über die Kompromisse für JEDEN Ansatz oder die Fälle, in denen Sie einen über den anderen verwenden sollten
Almanegra

Vor- und Nachteile bieten einen Kompromiss, da Vor- und Nachteile der Vererbung Nachteile der Zusammensetzung und Nachteile der Zusammensetzung Vor- und Nachteile der Vererbung sind.
Ravindra Babu

1

Wie viele Leute sagten, werde ich zuerst mit der Überprüfung beginnen - ob es eine "Ist-Eine" -Beziehung gibt. Wenn es existiert, überprüfe ich normalerweise Folgendes:

Gibt an, ob die Basisklasse instanziiert werden kann. Das heißt, ob die Basisklasse nicht abstrakt sein kann. Wenn es nicht abstrakt sein kann, bevorzuge ich normalerweise Komposition

ZB 1. Buchhalter ist ein Mitarbeiter. Ich werde jedoch keine Vererbung verwenden, da ein Employee-Objekt instanziiert werden kann.

ZB 2. Buch ist ein Verkaufsartikel. Ein SellingItem kann nicht instanziiert werden - es ist ein abstraktes Konzept. Daher werde ich Vererbung verwenden. Das SellingItem ist eine abstrakte Basisklasse (oder Schnittstelle in C #).

Was denkst du über diesen Ansatz?

Außerdem unterstütze ich die Antwort von @anon in Warum überhaupt Vererbung verwenden?

Der Hauptgrund für die Verwendung der Vererbung liegt nicht in der Form der Komposition, sondern darin, dass Sie polymorphes Verhalten erhalten können. Wenn Sie keinen Polymorphismus benötigen, sollten Sie wahrscheinlich keine Vererbung verwenden.

@MatthieuM. sagt in /software/12439/code-smell-inheritance-abuse/12448#comment303759_12448

Das Problem bei der Vererbung ist, dass sie für zwei orthogonale Zwecke verwendet werden kann:

Schnittstelle (für Polymorphismus)

Implementierung (zur Wiederverwendung von Code)

REFERENZ

  1. Welches Klassendesign ist besser?
  2. Vererbung vs. Aggregation

1
Ich bin mir nicht sicher, warum 'die Basisklasse abstrakt ist?' Zahlen in die Diskussion .. der LSP: Funktionieren alle Funktionen, die bei Hunden funktionieren, wenn Pudelobjekte übergeben werden? Wenn ja, kann Pudel anstelle von Hund verwendet werden und somit von Hund erben.
Gishu

@ Gishu Danke. Ich werde auf jeden Fall in LSP schauen. Aber bitte geben Sie vorher ein "Beispiel, in dem die Vererbung richtig ist, in der die Basisklasse nicht abstrakt sein kann". Ich denke, Vererbung ist nur anwendbar, wenn die Basisklasse abstrakt ist. Wenn die Basisklasse separat instanziiert werden muss, streben Sie keine Vererbung an. Das heißt, obwohl der Buchhalter ein Mitarbeiter ist, verwenden Sie keine Vererbung.
LCJ

1
habe in letzter Zeit WCF gelesen. Ein Beispiel im .net-Framework ist SynchronizationContext (base + kann instanziiert werden), dessen Warteschlangen auf einen ThreadPool-Thread angewendet werden. Ableitungen umfassen WinFormsSyncContext (Warteschlange auf UI-Thread) und DispatcherSyncContext (Warteschlange auf WPF Dispatcher)
Gishu

@ Gishu Danke. Es wäre jedoch hilfreicher, wenn Sie ein Szenario bereitstellen könnten, das auf der Bankdomäne, der HR-Domäne, der Einzelhandelsdomäne oder einer anderen beliebten Domäne basiert.
LCJ

1
Es tut uns leid. Ich bin mit diesen Domänen nicht vertraut. Ein weiteres Beispiel, wenn die vorherige zu stumpf war, ist die Control-Klasse in Winforms / WPF. Die Basis- / generische Steuerung kann instanziiert werden. Ableitungen umfassen Listboxes, Textboxes usw. Nun, da ich darüber nachdenke, ist das Decorator Design-Muster meiner Meinung nach ein gutes Beispiel und auch nützlich. Der Dekorateur leitet sich von dem nicht abstrakten Objekt ab, das er einwickeln / dekorieren möchte.
Gishu

1

Ich sehe, dass niemand das Diamantproblem erwähnt hat , das bei der Vererbung auftreten könnte.

Auf einen Blick, wenn die Klassen B und C A erben und beide die Methode X überschreiben und eine vierte Klasse D sowohl von B als auch von C erbt und X nicht überschreibt, welche Implementierung von XD soll verwendet werden?

Wikipedia bietet einen schönen Überblick über das Thema, das in dieser Frage diskutiert wird.


1
D erbt B und C nicht A. Wenn ja, dann würde es die Implementierung von X verwenden, die in Klasse A ist.
Fabricio

1
@ Fabricio: Danke, ich habe den Text bearbeitet. Ein solches Szenario kann übrigens nicht in Sprachen auftreten, die keine Vererbung mehrerer Klassen zulassen. Stimmt das?
Veverke

Ja, Sie haben Recht. Und ich habe noch nie mit einer gearbeitet, die Mehrfachvererbung zulässt (gemäß dem Beispiel im Diamantproblem).
Fabricio
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.