Warum Komposition gegenüber Vererbung bevorzugen? Welche Kompromisse gibt es für jeden Ansatz? Wann sollten Sie die Vererbung der Komposition vorziehen?
language-agnostic
oop
inheritance
composition
aggregation
Schreibgeschützt
quelle
quelle
Antworten:
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 .
Will TypeB nur einen Teil des von TypeA offenbarten Verhaltens? Zeigt den Bedarf an Zusammensetzung an.
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?"
quelle
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 .
quelle
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).
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:
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:
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: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.
quelle
Bei all den unbestreitbaren Vorteilen, die die Vererbung bietet, sind hier einige ihrer Nachteile aufgeführt.
Nachteile der Vererbung:
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.
quelle
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.
quelle
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.
quelle
InternalCombustionEngine
mit einer abgeleiteten KlasseGasolineEngine
. Letzteres fügt Dinge wie Zündkerzen hinzu, die der Basisklasse fehlen, aber wenn Sie das Ding als verwenden,InternalCombustionEngine
werden die Zündkerzen verwendet.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.
quelle
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.
quelle
Client
. DannPreferredClient
taucht später ein neues Konzept eines auf. SolltePreferredClient
erbenClient
? 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 dieclient.makePreferred()
Operation modellieren ? Vielleicht liegt die Antwort in der Verwendung von Komposition mit einem fehlenden Konzept,Account
vielleicht?Client
Klassen zu haben, gibt es vielleicht nur eine, die das Konzept einer Klasse zusammenfasst,Account
die eineStandardAccount
oder eine sein könntePreferredAccount
...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
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.
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:
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.
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.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.
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 .
Die Komposition hat den Vorteil einer kombinatororientierten Programmierung, dh sie funktioniert ähnlich wie das zusammengesetzte Muster .
Die Komposition folgt unmittelbar auf die Programmierung einer Schnittstelle .
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.
quelle
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.
quelle
TextFile
ist einFile
.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:
quelle
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
Bar
erben kann,Foo
wenn der folgende Satz wahr ist:Leider ist der obige Test allein nicht zuverlässig. Verwenden Sie stattdessen Folgendes:
Der erste Test stellt sicher , dass alle Getter von
Foo
Sinne inBar
(= shared Eigenschaften), während der zweite Test stellt sicher , dass alle Setter vonFoo
Sinne inBar
(= 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 erbenAnimal
.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 erbenEllipse
.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
Widget
in GUI-Frameworks oder eine polymorphe KlasseNode
in 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 :
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".
quelle
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.computeArea(Circle *c) { return pi * width * height / 4.0; }
Jetzt ist es generisch.width()
undheight()
? 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.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.
quelle
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).
quelle
Inheritance is sometimes useful... That way you can have polymorphism
so 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.Angenommen, ein Flugzeug besteht nur aus zwei Teilen: einem Motor und Flügeln.
Dann gibt es zwei Möglichkeiten, eine Flugzeugklasse zu entwerfen.
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
Engine
einen Mutator verfügbar, um seineEigenschaften zu ändern , oder ich gestalte ihn neu
Aircraft
als:Jetzt kann ich meinen Motor auch im laufenden Betrieb austauschen.
quelle
Sie müssen sich das Liskov-Substitutionsprinzip in Onkel Bobs SOLID- Prinzipien des Klassendesigns ansehen . :) :)
quelle
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.
quelle
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:
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.
quelle
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.
Es klingt großartig, aber in der Praxis funktioniert es aus einem von mehreren Gründen fast nie.
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.
quelle
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.
quelle
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.
quelle
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).
quelle
Die Subtypisierung ist angemessen und leistungsfähiger, wenn die Invarianten aufgelistet werden können , andernfalls wird die Funktionszusammensetzung zur Erweiterbarkeit verwendet.
quelle
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.
Wenn Sie jedoch nur die Wiederverwendung von Code beabsichtigen, ist die Komposition höchstwahrscheinlich die bessere Wahl für das Design.
quelle
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):
Dann schreibe ich die Menge der ganzen Zahlen als Unterklasse der Liste der ganzen Zahlen:
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 ).
quelle
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.
quelle
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.
quelle
Obwohl Zusammensetzung bevorzugt ist, würde Ich mag Profis von hervorzuheben Vererbung und Nachteile der Zusammensetzung .
Vorteile der Vererbung:
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
Mit der Vererbung können Sie eine Funktion definieren / ändern / erweitern
Nachteile der Zusammensetzung:
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
quelle
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:
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?
@MatthieuM. sagt in /software/12439/code-smell-inheritance-abuse/12448#comment303759_12448
REFERENZ
quelle
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.
quelle