In den letzten Monaten scheint das Mantra "Komposition gegenüber Vererbung bevorzugen" aus dem Nichts aufgetaucht zu sein und fast zu einer Art Mem innerhalb der Programmgemeinschaft geworden zu sein. Und jedes Mal, wenn ich es sehe, bin ich ein bisschen verwirrt. Es ist, als ob jemand gesagt hätte "zugunsten von Bohrern gegenüber Hämmern". Nach meiner Erfahrung sind Komposition und Vererbung zwei verschiedene Werkzeuge mit unterschiedlichen Anwendungsfällen, und es macht keinen Sinn, sie so zu behandeln, als wären sie austauschbar und eines war dem anderen von Natur aus überlegen.
Außerdem sehe ich keine wirkliche Erklärung dafür, warum Vererbung schlecht und Zusammensetzung gut ist, was mich nur misstrauischer macht. Soll es nur im Glauben akzeptiert werden? Liskov-Substitution und Polymorphismus haben bekannte, eindeutige Vorteile, und IMO umfasst den gesamten Punkt der Verwendung objektorientierter Programmierung, und niemand erklärt jemals, warum sie zugunsten der Komposition verworfen werden sollten.
Weiß jemand, woher dieses Konzept kommt und welche Gründe dahinter stehen?
quelle
Antworten:
Obwohl ich glaube, dass ich schon lange vor GoF Diskussionen über Komposition und Vererbung gehört habe, kann ich nicht auf eine bestimmte Quelle eingehen. Könnte sowieso Booch gewesen sein.
<rant>
Ah, aber wie viele Mantras ist auch dieses nach typischen Gesichtspunkten degeneriert:
Das Mem, das ursprünglich dazu gedacht war, n00bs zur Erleuchtung zu führen, wird jetzt als Keule verwendet, um sie bewusstlos zu machen.
Zusammensetzung und Vererbung sind sehr unterschiedliche Dinge und sollten nicht miteinander verwechselt werden. Es ist zwar richtig, dass Komposition verwendet werden kann, um Vererbung mit viel zusätzlicher Arbeit zu simulieren , dies macht Vererbung jedoch weder zu einem Bürger zweiter Klasse, noch macht es Komposition zum bevorzugten Sohn. Die Tatsache, dass viele n00bs versuchen, Vererbung als Abkürzung zu verwenden, macht den Mechanismus nicht ungültig, und fast alle n00bs lernen aus dem Fehler und verbessern sich dadurch.
Bitte denkt über eure Entwürfe nach und hört auf, Slogans auszusprechen.
</ rant>
quelle
Erfahrung.
Wie Sie sagen, sind sie Werkzeuge für verschiedene Jobs, aber der Satz kam zustande, weil die Leute ihn nicht auf diese Weise benutzten.
Vererbung ist in erster Linie ein polymorphes Werkzeug, aber einige Leute versuchen, es zu verwenden, um Code wiederzuverwenden / weiterzugeben. Das Grundprinzip lautet: "Gut, wenn ich erbe, bekomme ich alle Methoden umsonst", ignoriert jedoch die Tatsache, dass diese beiden Klassen möglicherweise keine polymorphe Beziehung haben.
Warum also die Komposition der Vererbung vorziehen - na ja, weil die Beziehung zwischen Klassen meist nicht polymorph ist. Es existiert einfach, um die Leute daran zu erinnern, dass sie nicht mit einem Knie-Ruck reagieren, indem sie erben.
quelle
Es ist keine neue Idee, ich glaube, sie wurde tatsächlich in das 1994 erschienene GoF Design Patterns Book aufgenommen.
Das Hauptproblem bei der Vererbung ist, dass es sich um eine White-Box handelt. Per Definition müssen Sie die Implementierungsdetails der Klasse kennen, von der Sie erben. Bei der Komposition hingegen interessiert Sie nur die öffentliche Schnittstelle der Klasse, die Sie komponieren.
Aus dem GoF-Buch:
Der Wikipedia-Artikel zum GoF-Buch hat eine anständige Einführung.
quelle
Um einen Teil Ihrer Frage zu beantworten, ist dieses Konzept meines Erachtens zum ersten Mal in dem 1994 erschienenen Buch GOF Design Patterns: Elements of Reusable Object-Oriented Software enthalten.
Sie stellen dieser Aussage einen kurzen Vergleich zwischen Vererbung und Komposition vor. Sie sagen nicht "Niemals Vererbung verwenden".
quelle
"Komposition über Vererbung" ist eine kurze (und anscheinend irreführende) Redewendung. "Wenn Sie der Meinung sind, dass die Daten (oder das Verhalten) einer Klasse in eine andere Klasse integriert werden sollten, ziehen Sie immer die Verwendung von Komposition in Betracht, bevor Sie blind Vererbung anwenden."
Warum ist das so? Da durch die Vererbung eine enge Kopplung zwischen den beiden Klassen während der Kompilierung entsteht. Im Gegensatz dazu ist die Zusammensetzung eine lose Kopplung, die unter anderem eine klare Trennung von Bedenken, die Möglichkeit des Wechsels von Abhängigkeiten zur Laufzeit und eine einfachere, isoliertere Abhängigkeitsprüfbarkeit ermöglicht.
Das bedeutet nur, dass die Vererbung mit Sorgfalt behandelt werden sollte, da sie mit Kosten verbunden ist und nicht, dass sie nicht nützlich ist. Tatsächlich ist "Komposition über Vererbung" häufig "Komposition + Vererbung über Vererbung", da Sie häufig möchten, dass Ihre komponierte Abhängigkeit eine abstrakte Oberklasse und nicht die konkrete Unterklasse selbst ist. Damit können Sie zur Laufzeit zwischen verschiedenen konkreten Implementierungen Ihrer Abhängigkeit wechseln.
Aus diesem Grund werden Sie (unter anderem) wahrscheinlich feststellen, dass Vererbung häufiger in Form von Schnittstellenimplementierung oder abstrakten Klassen verwendet wird als Vanilla-Vererbung.
Ein (metaphorisches) Beispiel könnte sein:
"Ich habe eine Snake-Klasse und möchte als Teil dieser Klasse angeben, was passiert, wenn die Snake beißt. Ich wäre versucht, die Snake eine BiterAnimal-Klasse mit der Bite () -Methode erben zu lassen und diese Methode zu überschreiben, um giftigen Biss widerzuspiegeln Aber Komposition über Vererbung warnt mich, dass ich stattdessen versuchen sollte, Komposition zu verwenden ... In meinem Fall könnte dies bedeuten, dass die Snake ein Bite-Mitglied hat. Die Bite-Klasse könnte abstrakt (oder eine Schnittstelle) mit mehreren Unterklassen sein Ich habe nette Dinge wie VenomousBite- und DryBite-Unterklassen und die Möglichkeit, den Biss in derselben Snake-Instanz zu ändern, in der die Schlange mit zunehmendem Alter wächst. Außerdem kann ich den Effekt eines Bisses in einer eigenen Klasse wieder in dieser Frost-Klasse verwenden , weil Frost beißt, aber kein BiterAnimal ist, und so weiter ... "
quelle
Einige mögliche Argumente für die Zusammensetzung:
Komposition ist etwas sprach- / rahmenunabhängiger
Vererbung und was sie erzwingt / erfordert / aktiviert, unterscheidet sich zwischen den Sprachen in Bezug auf den Zugriff auf die Unter- / Oberklasse und die Auswirkungen auf die Leistung, die sie auf virtuelle Methoden usw. haben kann. Komposition ist recht einfach und erfordert Sehr geringe Sprachunterstützung und damit Implementierungen über verschiedene Plattformen / Frameworks hinweg können Kompositionsmuster einfacher gemeinsam nutzen.
Komposition ist eine sehr einfache und taktile Art, Objekte zu bauen
Vererbung ist relativ leicht zu verstehen, aber im wirklichen Leben noch nicht so leicht nachzuweisen. Viele Gegenstände im wirklichen Leben können in Teile zerlegt und zusammengesetzt werden. Angenommen, ein Fahrrad kann mit zwei Rädern, einem Rahmen, einem Sitz, einer Kette usw. gebaut werden. Während in einer Erbschaftsmetapher gesagt werden kann, dass ein Fahrrad ein Einrad ausdehnt, was zwar machbar ist, aber noch viel weiter vom tatsächlichen Bild entfernt ist als die Komposition (dies ist offensichtlich kein sehr gutes Erbschaftsbeispiel, aber der Punkt bleibt derselbe). Sogar die Wortvererbung (zumindest von den meisten US-Englischsprechern, die ich erwarten würde) ruft automatisch eine Bedeutung in der Art "Etwas, das von einem verstorbenen Verwandten überliefert wurde" auf, die in gewisser Weise mit seiner Bedeutung in der Software korreliert, aber immer noch nur lose passt.
Komposition ist fast immer flexibler
Mit Komposition können Sie jederzeit Ihr eigenes Verhalten definieren oder einfach diesen Teil Ihrer komponierten Teile freigeben. Auf diese Weise unterliegen Sie keiner der Einschränkungen, die durch eine Vererbungshierarchie (virtuell oder nicht virtuell usw.) auferlegt werden können.
Dies könnte daran liegen, dass die Komposition natürlich eine einfachere Metapher ist, die weniger theoretische Einschränkungen als die Vererbung aufweist. Darüber hinaus können diese besonderen Gründe während der Entwurfszeit offensichtlicher sein oder möglicherweise im Umgang mit einigen der Schmerzpunkte der Vererbung auffallen.
Disclaimer:
Offensichtlich ist es nicht diese klare Einbahnstraße. Jedes Design verdient die Bewertung mehrerer Muster / Werkzeuge. Vererbung ist weit verbreitet, hat viele Vorteile und ist oft eleganter als Komposition. Dies sind nur einige mögliche Gründe, die für die Komposition sprechen könnten.
quelle
Vielleicht haben Sie in den letzten Monaten gerade bemerkt, dass Leute dies sagten, aber es ist guten Programmierern schon viel länger bekannt. Ich sage es mit Sicherheit seit etwa einem Jahrzehnt, wo es angebracht ist.
Der Sinn des Konzepts ist, dass die Vererbung einen großen konzeptionellen Aufwand mit sich bringt. Wenn Sie die Vererbung verwenden, enthält jeder einzelne Methodenaufruf einen impliziten Versand. Wenn Sie über tiefe Vererbungsbäume oder mehrere oder (noch schlimmer) beides verfügen, kann es zu einer königlichen PITA werden, wenn Sie herausfinden, wohin die bestimmte Methode in einem bestimmten Aufruf gesendet wird. Dadurch wird die korrekte Begründung des Codes komplexer und das Debuggen schwieriger.
Lassen Sie mich ein einfaches Beispiel zur Veranschaulichung geben. Angenommen, tief in einem Vererbungsbaum hat jemand eine Methode benannt
foo
. Dann kommt jemand anderes und fügtfoo
oben am Baum etwas hinzu , macht aber etwas anderes. (Dieser Fall tritt häufiger bei Mehrfachvererbung auf.) Diese Person, die in der Stammklasse arbeitet, hat die obskure untergeordnete Klasse beschädigt und erkennt sie wahrscheinlich nicht. Sie könnten eine 100-prozentige Abdeckung mit Komponententests haben und diesen Bruch nicht bemerken, da die Person an der Spitze nicht daran denkt, die untergeordnete Klasse zu testen, und die Tests für die untergeordnete Klasse nicht daran, die oben erstellten neuen Methoden zu testen . (Zugegeben, es gibt Möglichkeiten, Komponententests zu schreiben, die dies auffangen, aber es gibt auch Fälle, in denen es nicht einfach ist, Tests so zu schreiben.)Wenn Sie dagegen Komposition verwenden, wird bei jedem Anruf in der Regel klarer, an wen Sie den Anruf weiterleiten. (OK, wenn Sie eine Invertierung der Steuerung verwenden, z. B. mit Abhängigkeitsinjektion, kann es auch problematisch sein, herauszufinden, wohin der Aufruf geht. In der Regel ist es jedoch einfacher, dies herauszufinden.) Dies erleichtert die Überlegung. Als Bonus führt die Komposition dazu, dass Methoden voneinander getrennt werden. Das obige Beispiel sollte dort nicht vorkommen, da die untergeordnete Klasse zu einer undurchsichtigen Komponente übergehen würde und es nie eine Frage gibt, ob der Aufruf von
foo
für die undurchsichtige Komponente oder das Hauptobjekt bestimmt war.Jetzt haben Sie vollkommen recht, dass Vererbung und Zusammensetzung zwei sehr unterschiedliche Werkzeuge sind, die zwei verschiedenen Arten von Dingen dienen. Sicher, Vererbung ist mit konzeptionellem Aufwand verbunden, aber wenn es sich um das richtige Tool für den Job handelt, ist der konzeptionelle Aufwand geringer als der Versuch, es nicht zu verwenden und von Hand zu tun, was es für Sie bedeutet. Niemand, der weiß, was er tut, würde sagen, dass Sie niemals Vererbung verwenden sollten. Aber sei sicher, dass es das Richtige ist.
Leider lernen viele Entwickler etwas über objektorientierte Software, lernen etwas über Vererbung und setzen ihre neue Axt so oft wie möglich ein. Das bedeutet, dass sie versuchen, Vererbung zu verwenden, wenn Komposition das richtige Werkzeug war. Hoffentlich lernen sie mit der Zeit besser, aber häufig geschieht dies erst nach ein paar entfernten Gliedmaßen usw. Wenn man ihnen vorher sagt, dass es eine schlechte Idee ist, kann dies den Lernprozess beschleunigen und Verletzungen reduzieren.
quelle
Es ist eine Reaktion auf die Beobachtung, dass OO-Anfänger dazu neigen, Vererbung zu verwenden, wenn sie dies nicht müssen. Vererbung ist sicherlich keine schlechte Sache, kann aber überbeansprucht werden. Wenn eine Klasse nur Funktionalität von einer anderen benötigt, funktioniert die Komposition wahrscheinlich. Unter anderen Umständen funktioniert die Vererbung und die Komposition nicht.
Das Erben aus einer Klasse impliziert viele Dinge. Dies impliziert, dass ein Abgeleiteter eine Art von Basis ist (siehe das Liskov-Substitutionsprinzip für die wichtigsten Details). Wenn Sie eine Basis verwenden, ist es sinnvoll, einen Abgeleiteten zu verwenden. Es gibt dem Abgeleiteten Zugriff auf die geschützten Mitglieder und Mitgliederfunktionen von Base. Es handelt sich um eine enge Beziehung, was bedeutet, dass eine hohe Kopplung besteht und Änderungen an einer wahrscheinlich Änderungen an der anderen erfordern.
Kopplung ist eine schlechte Sache. Es erschwert das Verstehen und Ändern von Programmen. Wenn andere Dinge gleich sind, sollten Sie immer die Option mit weniger Kopplung auswählen.
Wenn also Komposition oder Vererbung die Aufgabe effektiv erledigen, wählen Sie Komposition, da es sich um eine geringere Kopplung handelt. Wenn die Komposition die Aufgabe nicht effektiv erledigt und die Vererbung die Aufgabe übernimmt, wählen Sie die Vererbung, da dies erforderlich ist.
quelle
Hier sind meine zwei Cent (abgesehen von all den bereits angesprochenen hervorragenden Punkten):
IMHO, es kommt auf die Tatsache an, dass die meisten Programmierer nicht wirklich Vererbung bekommen und es am Ende übertreiben, teilweise aufgrund dessen, wie dieses Konzept gelehrt wird. Dieses Konzept dient dazu, sie davon abzubringen, es zu übertreiben, anstatt sich darauf zu konzentrieren, ihnen beizubringen, wie man es richtig macht.
Jeder, der Zeit mit Unterrichten oder Mentoring verbracht hat, weiß, dass dies häufig der Fall ist, insbesondere bei neuen Entwicklern, die Erfahrung mit anderen Paradigmen haben:
Diese Entwickler glauben zunächst, dass Vererbung dieses beängstigende Konzept ist. Am Ende erstellen sie Typen mit Schnittstellenüberlappungen (z. B. dasselbe festgelegte Verhalten ohne gemeinsame Untertypen) und mit globalen Elementen zur Implementierung gemeinsamer Funktionen.
Dann lernen sie (oft als Folge von übereifrigem Lehren) die Vorteile der Vererbung kennen, aber es wird oft als die Allheilmittel für die Wiederverwendung gelehrt. Sie enden mit der Vorstellung, dass jedes gemeinsame Verhalten durch Vererbung geteilt werden muss. Dies liegt daran, dass der Schwerpunkt häufig auf der Vererbung der Implementierung und nicht auf der Untertypisierung liegt.
In 80% der Fälle ist das gut genug. Bei den anderen 20% beginnt das Problem. Um ein Umschreiben zu vermeiden und sicherzustellen, dass sie die Vorteile der gemeinsamen Implementierung nutzen, beginnen sie, ihre Hierarchie eher nach der beabsichtigten Implementierung als nach den zugrunde liegenden Abstraktionen zu gestalten. Der "Stapel erbt von doppelt verknüpfter Liste" ist ein gutes Beispiel dafür.
Die meisten Lehrer bestehen an dieser Stelle nicht darauf, das Konzept der Benutzeroberflächen einzuführen, da es sich entweder um ein anderes Konzept handelt oder Sie (in C ++) sie mit abstrakten Klassen und Mehrfachvererbung fälschen müssen, was in dieser Phase noch nicht gelehrt wird. In Java unterscheiden viele Lehrer "keine Mehrfachvererbung" oder "Mehrfachvererbung ist böse" nicht von der Bedeutung von Schnittstellen.
All dies wird durch die Tatsache verstärkt, dass wir so viel von der Schönheit gelernt haben, nicht überflüssigen Code mit Implementierungsvererbung schreiben zu müssen, dass Tonnen von einfachem Delegierungscode unnatürlich aussehen. Die Komposition sieht also chaotisch aus, weshalb wir diese Faustregeln brauchen, um neue Programmierer dazu zu bringen, sie trotzdem zu verwenden (was sie auch übertreiben).
quelle
In einem Kommentar erwähnte Mason, dass wir eines Tages über Vererbung sprechen würden, die als schädlich angesehen wird .
Hoffentlich.
Das Problem mit der Vererbung ist auf einmal einfach und tödlich, es respektiert nicht die Idee, dass ein Konzept eine Funktionalität haben sollte.
In den meisten OO-Sprachen haben Sie beim Erben von einer Basisklasse folgende Möglichkeiten:
Und hier wird der Ärger.
Dies ist nicht der einzige Ansatz, obwohl 00-Sprachen meistens daran hängen bleiben. Glücklicherweise gibt es in diesen Interfaces / abstrakte Klassen.
Auch die mangelnde Leichtigkeit, etwas anderes zu tun, trägt dazu bei, dass es größtenteils genutzt wird: Selbst wenn Sie dies wissen, würden Sie von einer Schnittstelle erben und die Basisklasse durch Komposition einbetten und die meisten Methodenaufrufe delegieren? Es wäre jedoch erheblich besser, Sie wären sogar gewarnt, wenn plötzlich eine neue Methode in der Benutzeroberfläche auftaucht und Sie bewusst auswählen müssten, wie sie implementiert werden soll.
Als Gegenpunkt,
Haskell
erlaubt nur das Liskov Prinzip zu verwenden , wenn „Ableiten“ von reinen Schnittstellen (genannt Konzepten) (1). Sie können nicht von einer vorhandenen Klasse abgeleitet werden, nur die Komposition ermöglicht das Einbetten der Daten.(1) Konzepte können einen sinnvollen Standard für eine Implementierung darstellen. Da sie jedoch keine Daten enthalten, kann dieser Standard nur im Hinblick auf die anderen vom Konzept vorgeschlagenen Methoden oder im Hinblick auf Konstanten definiert werden.
quelle
Die einfache Antwort lautet: Vererbung hat eine größere Kopplung als Komposition. Wählen Sie aus zwei Optionen mit ansonsten äquivalenten Eigenschaften die weniger gekoppelte aus.
quelle
Ich denke, diese Art von Rat ist wie das Sprichwort "lieber fahren als fliegen". Das heißt, Flugzeuge haben alle möglichen Vorteile gegenüber Autos, aber das ist mit einer gewissen Komplexität verbunden. Also , wenn viele Leute versuchen , vom Stadtzentrum in die Vororte zu fliegen, die Ratschläge , die sie wirklich, wirklich hören müssen, ist , dass sie nicht fliegen müssen, und in der Tat, fliegen wird es nur auf lange Sicht komplizierter machen, auch wenn es kurzfristig cool / effizient / einfach erscheint. Während , wenn Sie sie fliegen müssen, ist es in der Regel sein , offensichtlich soll.
Ebenso kann die Vererbung Dinge bewirken, die die Komposition nicht kann, aber Sie sollten diese verwenden, wenn Sie sie benötigen und nicht, wenn Sie dies nicht tun. Wenn Sie also nie versucht sind, einfach anzunehmen, dass Sie Vererbung benötigen, wenn Sie dies nicht tun, brauchen Sie nicht den Rat "Komposition bevorzugen". Aber viele Menschen tun , und tun , dass die Beratung benötigen.
Es soll implizit sein, dass wenn Sie wirklich Vererbung brauchen, es offensichtlich ist, und Sie sollten es dann verwenden.
Auch Steven Lowes Antwort. Wirklich, wirklich das.
quelle
Vererbung ist nicht von Natur aus schlecht und Zusammensetzung ist nicht von Natur aus gut. Sie sind lediglich Werkzeuge, mit denen ein OO-Programmierer Software entwerfen kann.
Wenn Sie sich eine Klasse ansehen, tut sie mehr, als sie unbedingt tun sollte ( SRP )? Ist es unnötig, Funktionen zu duplizieren ( DRY ), oder ist es übermäßig an den Eigenschaften oder Methoden anderer Klassen interessiert ( Feature Envy )? Wenn die Klasse all diese Konzepte (und möglicherweise mehr) zu verletzen, ist es den Versuch , eine sein , Gottes Klasse . Dies sind eine Reihe von Problemen, die beim Entwerfen von Software auftreten können, von denen keines notwendigerweise ein Vererbungsproblem ist, die jedoch schnell zu starken Kopfschmerzen und spröden Abhängigkeiten führen können, wenn auch Polymorphismus angewendet wurde.
Die Ursache für das Problem dürfte weniger ein mangelndes Verständnis der Vererbung sein, sondern eher eine schlechte Wahl im Hinblick auf das Design oder das Nichterkennen von "Codegerüchen" in Bezug auf Klassen, die sich nicht an das Prinzip der einheitlichen Verantwortung halten . Polymorphismus und Liskov-Substitution müssen nicht zugunsten der Zusammensetzung verworfen werden. Der Polymorphismus selbst kann angewendet werden, ohne auf die Vererbung angewiesen zu sein. Dies sind alles recht komplementäre Konzepte. Wenn nachdenklich angewendet. Der Trick besteht darin, Ihren Code einfach und sauber zu halten und sich nicht übermäßig Gedanken über die Anzahl der Klassen zu machen, die Sie erstellen müssen, um ein robustes Systemdesign zu erstellen.
Was die Bevorzugung der Komposition gegenüber der Vererbung anbelangt, so ist dies nur ein weiterer Fall der durchdachten Anwendung der Gestaltungselemente, die im Hinblick auf das zu lösende Problem am sinnvollsten sind. Wenn Sie nicht brauchen Verhalten zu erben, dann sollten Sie sich vielleicht nicht als Zusammensetzung wird dazu beitragen , Unvereinbarkeiten zu vermeiden und erhebliche Anstrengungen Refactoring später. Wenn Sie andererseits feststellen, dass Sie viel Code wiederholen, sodass sich die gesamte Verdoppelung auf eine Gruppe ähnlicher Klassen konzentriert, kann das Erstellen eines gemeinsamen Vorfahren dazu beitragen, die Anzahl identischer Aufrufe und Klassen zu verringern Möglicherweise müssen Sie zwischen den einzelnen Klassen wiederholen. Sie bevorzugen also die Komposition, gehen aber nicht davon aus, dass die Vererbung niemals anwendbar ist.
quelle
Das eigentliche Zitat stammt, glaube ich, aus Joshua Blochs "Effective Java", wo es der Titel eines der Kapitel ist. Er behauptet, dass die Vererbung innerhalb eines Pakets und überall dort sicher ist, wo eine Klasse speziell für die Erweiterung entworfen und dokumentiert wurde. Er behauptet, wie andere angemerkt haben, dass die Vererbung die Kapselung unterbricht, da eine Unterklasse von den Implementierungsdetails ihrer Oberklasse abhängt. Dies führe zu Zerbrechlichkeit.
Obwohl er es nicht erwähnt, scheint es mir, dass die einmalige Vererbung in Java bedeutet, dass die Komposition Ihnen viel mehr Flexibilität bietet als die Vererbung.
quelle