1984 Stimmen

Bevorzugen Sie Komposition statt Vererbung?

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

6 Stimmen

6 Stimmen

In einem Satz Vererbung ist öffentlich, wenn Sie eine öffentliche Methode haben und Sie ändern es ändert die veröffentlichte api. wenn Sie Zusammensetzung haben und das Objekt komponiert hat sich geändert, müssen Sie nicht Ihre veröffentlichte api ändern.

39voto

Mecki Punkte 113876

Ich persönlich habe gelernt, die Komposition immer der Vererbung vorzuziehen. Es gibt kein programmatisches Problem, das Sie mit Vererbung lösen können, das Sie nicht mit Komposition lösen können; obwohl Sie in einigen Fällen Schnittstellen (Java) oder Protokolle (Obj-C) verwenden müssen. Da C++ so etwas nicht kennt, müssen Sie abstrakte Basisklassen verwenden, was bedeutet, dass Sie die Vererbung in C++ nicht ganz abschaffen können.

Komposition ist oft logischer, sie bietet eine bessere Abstraktion, eine bessere Kapselung, eine bessere Wiederverwendung des Codes (vor allem bei sehr großen Projekten) und es ist unwahrscheinlicher, dass irgendetwas aus der Ferne kaputt geht, nur weil Sie irgendwo in Ihrem Code eine isolierte Änderung vorgenommen haben. Es macht es auch einfacher, die " Prinzip der einzigen Verantwortung ", das oft als " " zusammengefaßt wird Es sollte nie mehr als einen Grund für den Wechsel einer Klasse geben. "Das bedeutet, dass jede Klasse für einen bestimmten Zweck existiert und nur Methoden haben sollte, die in direktem Zusammenhang mit ihrem Zweck stehen. Außerdem ist es mit einem sehr flachen Vererbungsbaum viel einfacher, den Überblick zu behalten, selbst wenn Ihr Projekt sehr groß wird. Viele Leute denken, dass Vererbung unser reale Welt ziemlich gut, aber das ist nicht die Wahrheit. In der realen Welt wird viel mehr komponiert als vererbt. So gut wie jedes Objekt der realen Welt, das Sie in der Hand halten können, wurde aus anderen, kleineren Objekten der realen Welt zusammengesetzt.

Die Zusammensetzung hat aber auch Nachteile. Wenn Sie die Vererbung ganz weglassen und sich nur auf die Komposition konzentrieren, werden Sie feststellen, dass Sie oft ein paar zusätzliche Codezeilen schreiben müssen, die nicht nötig wären, wenn Sie die Vererbung verwendet hätten. Außerdem ist man manchmal gezwungen, sich zu wiederholen, was gegen die DRY-Prinzip (DRY = Don't Repeat Yourself). Auch Komposition erfordert oft Delegation, und eine Methode ruft einfach eine andere Methode eines anderen Objekts auf, ohne dass dieser Aufruf von anderem Code umgeben ist. Solche "doppelten Methodenaufrufe" (die sich leicht auf drei- oder vierfache Methodenaufrufe und sogar noch weiter ausdehnen können) haben eine viel schlechtere Leistung als die Vererbung, bei der Sie einfach eine Methode Ihres Elternteils erben. Der Aufruf einer geerbten Methode kann genauso schnell sein wie der Aufruf einer nicht geerbten Methode, oder er kann etwas langsamer sein, ist aber normalerweise immer noch schneller als zwei aufeinanderfolgende Methodenaufrufe.

Sie haben vielleicht bemerkt, dass die meisten OO-Sprachen keine Mehrfachvererbung zulassen. Es gibt zwar ein paar Fälle, in denen man durch Mehrfachvererbung wirklich etwas gewinnen kann, aber das sind eher Ausnahmen als die Regel. Wann immer Sie auf eine Situation stoßen, in der Sie denken "Mehrfachvererbung wäre eine wirklich coole Funktion, um dieses Problem zu lösen", sind Sie normalerweise an einem Punkt, an dem Sie die Vererbung insgesamt überdenken sollten, denn selbst wenn es ein paar zusätzliche Codezeilen erfordert, wird sich eine auf Komposition basierende Lösung normalerweise als viel eleganter, flexibler und zukunftssicherer erweisen.

Vererbung ist wirklich ein cooles Feature, aber ich fürchte, es wurde in den letzten Jahren überstrapaziert. Die Leute haben die Vererbung als den einen Hammer behandelt, mit dem man alles festnageln kann, unabhängig davon, ob es sich tatsächlich um einen Nagel, eine Schraube oder vielleicht um etwas ganz anderes handelt.

0 Stimmen

"Viele Leute denken, dass das Erbe unsere reale Welt ziemlich gut repräsentiert, aber das ist nicht die Wahrheit." So viel dazu! Im Gegensatz zu so ziemlich allen Programmier-Tutorials auf der Welt ist die Modellierung von Objekten in der realen Welt als Vererbungsketten auf lange Sicht wahrscheinlich eine schlechte Idee. Sie sollten Vererbung nur dann verwenden, wenn es eine unglaublich offensichtliche, angeborene, einfache Ist-ein-Beziehung gibt. Wie TextFile es un File .

0 Stimmen

@neonblitzer Sogar TextFile is-a File könnte ohne allzu große Probleme mit anderen Techniken modelliert werden. Zum Beispiel könnte man "File" zu einer Schnittstelle machen, mit konkreten TextFile- und BinaryFile-Implementierungen, oder, vielleicht ist "File" eine Klasse, die mit einer Instanz von BinaryFileBehaviors oder TextFileBehaviors instanziiert werden kann (d.h. unter Verwendung des Strategiemusters). Ich habe is-a aufgegeben und folge jetzt einfach dem Mantra "Vererbung ist ein letzter Ausweg, verwenden Sie es, wenn keine andere Option ausreichend funktioniert".

27voto

Peter Tseng Punkte 12727

Meine allgemeine Faustregel lautet: Bevor Sie die Vererbung einsetzen, sollten Sie überlegen, ob eine Komposition sinnvoller ist.

Der Grund: Unterklassifizierung bedeutet in der Regel mehr Komplexität und Vernetzung, d. h. es ist schwieriger, etwas zu ändern, zu pflegen und zu skalieren, ohne Fehler zu machen.

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

Häufige Probleme bei der Verwendung der Vererbung sind meiner Meinung nach folgende:

  • Unschuldige Handlungen können unerwartete Folgen haben - Das klassische Beispiel hierfür sind Aufrufe von überschreibbaren Methoden aus dem Konstruktor der Oberklasse Konstruktor, bevor die Instanzfelder der Unterklassen initialisiert wurden. In einer perfekten Welt würde das niemand tun. Dies ist nicht eine perfekte Welt.
  • Sie verleitet Unterklassifizierer dazu, Annahmen über die Reihenfolge von Methodenaufrufen und dergleichen zu treffen. - Solche Annahmen sind in der Regel nicht stabil zu sein, wenn sich die Oberklasse im Laufe der Zeit weiterentwickeln kann. Siehe auch mein Toaster und Kaffeekanne Analogie .
  • Die Klassen werden schwerer - Sie wissen nicht unbedingt, welche Arbeit Ihre Superklasse in ihrem Konstruktor verrichtet oder wie viel Speicher sie benötigt. verwenden wird. Das Konstruieren eines unschuldigen, vermeintlich leichten Objekts kann also viel teurer sein, als Sie denken, und das kann sich mit der Zeit ändern, wenn die Superklasse sich weiterentwickelt
  • Sie begünstigt eine Explosion von Unterklassen . Das Laden von Klassen kostet Zeit, mehr Klassen kosten Speicher. Das mag kein Problem sein, bis Sie es mit einer mit einer Anwendung in der Größenordnung von NetBeans zu tun hat, aber dort hatten wir echte Probleme, z. B. mit langsamen Menüs, weil die erste Anzeige eines Menüs eines Menüs ein massives Laden von Klassen auslöste. Wir haben dies durch den Wechsel zu deklarative Syntax und andere Techniken, aber auch das kostete Zeit zu beheben.
  • Das macht es schwieriger, Dinge später zu ändern. - wenn Sie eine Klasse öffentlich gemacht haben, wird das Austauschen der Oberklasse die Unterklassen kaputt machen - Es ist eine Entscheidung, an die man gebunden ist, sobald man den Code öffentlich gemacht hat. gebunden. Wenn Sie also die eigentliche Funktionalität Ihrer Superklasse nicht verändern Superklasse nicht ändert, hat man viel mehr Freiheit, Dinge später zu ändern, wenn man verwenden, anstatt das, was Sie brauchen, zu erweitern. Nehmen Sie zum Beispiel, Unterklassenbildung von JPanel - das ist normalerweise falsch; und wenn die Unterklasse öffentlich ist, haben Sie nie die Möglichkeit, diese Entscheidung zu revidieren. Wenn es als JComponent getThePanel() aufgerufen wird, können Sie es immer noch tun (Hinweis: Stellen Sie Modelle für die Komponenten darin als Ihre API zur Verfügung).
  • Objekthierarchien lassen sich nicht skalieren (oder es ist viel schwieriger, sie nachträglich zu skalieren als im Voraus zu planen) - das ist das klassische "zu viele Schichten"-Problem Problem. Darauf werde ich weiter unten eingehen, und wie das AskTheOracle-Muster lösen kann (obwohl es OOP-Puristen beleidigen könnte).

...

Meine Meinung dazu, was zu tun ist, wenn Sie eine Vererbung zulassen, was Sie tun können mit Vorsicht zu genießen ist:

  • Niemals Felder freilegen, außer Konstanten
  • Methoden müssen entweder abstrakt oder final sein
  • Keine Methoden aus dem Konstruktor der Oberklasse aufrufen

...

all dies gilt weniger für kleine als für große Projekte und weniger für private Klassen als für öffentliche

20voto

yukondude Punkte 23093

Vererbung ist sehr mächtig, aber man kann sie nicht erzwingen (siehe: die Kreis-Ellipsen-Problem ). Wenn Sie wirklich nicht sicher sein können, dass die Beziehung zwischen den Untertypen wirklich "ist-a" ist, ist es am besten, die Komposition zu verwenden.

18voto

Scotty Jamison Punkte 5635

Wenn Sie die kanonische, lehrbuchmäßige Antwort wollen, die die Leute seit dem Aufkommen von OOP geben (die Sie in diesen Antworten von vielen Leuten sehen), dann wenden Sie die folgende Regel an: "Wenn Sie eine is-a-Beziehung haben, verwenden Sie Vererbung. Wenn Sie eine has-a-Beziehung haben, verwenden Sie Komposition".

Das ist der übliche Ratschlag, und wenn Sie damit zufrieden sind, können Sie hier aufhören zu lesen und Ihren Weg fortsetzen. Für alle anderen...

is-a/has-a-Vergleiche haben Probleme

Zum Beispiel:

  • Ein Quadrat ist ein Rechteck, aber wenn Ihre Rechteckklasse eine setWidth() / setHeight() Methoden, dann gibt es keinen vernünftigen Weg, um eine Square erben von Rectangle ohne Bruch Das Liskovsche Substitutionsprinzip .
  • Eine ist-a-Beziehung kann oft so umformuliert werden, dass sie wie eine hat-a-Beziehung klingt. Zum Beispiel ist ein Angestellter eine Person, aber eine Person hat auch den Beschäftigungsstatus "angestellt".
  • is-a-Beziehungen können zu unschönen Mehrfachvererbungshierarchien führen, wenn man nicht aufpasst. Schließlich gibt es im Englischen keine Regel, die besagt, dass ein Objekt est genau eine Sache.
  • Die Leute sind schnell dabei, diese "Regel" weiterzugeben, aber hat jemals jemand versucht, sie zu untermauern oder zu erklären, warum es eine gute Heuristik ist, ihr zu folgen? Sicher, es passt gut zu der Idee, dass OOP die reale Welt modellieren soll, aber das ist an und für sich noch kein Grund, ein Prinzip zu übernehmen.

Voir diese StackOverflow-Frage für weitere Informationen zu diesem Thema.

Um zu wissen, wann man Vererbung und wann man Komposition verwenden sollte, muss man zunächst die Vor- und Nachteile der beiden Methoden kennen.

Die Probleme bei der Vererbung der Implementierung

Andere Antworten haben die Probleme mit der Vererbung sehr gut erklärt, so dass ich versuchen werde, hier nicht zu sehr ins Detail zu gehen. Aber hier ist eine kurze Liste:

  • Bei der Vererbung wird wohl ein gewisses Maß an Implementierungsdetails an Unterklassen weitergegeben (über geschützte Mitglieder).
  • Es kann schwierig sein, der Logik zu folgen, die sich zwischen den Methoden der Basis- und der Unterklassen bewegt.
  • El Problem der fragilen Basis . Erschwerend kommt hinzu, dass die meisten OOP-Sprachen standardmäßig Vererbung zulassen - API-Designer, die nicht proaktiv verhindern, dass andere Personen von ihren öffentlichen Klassen erben, müssen besonders vorsichtig sein, wenn sie ihre Basisklassen überarbeiten. Leider wird das Problem der fragilen Basisklassen oft missverstanden, was dazu führt, dass viele nicht verstehen, was es braucht, um eine Klasse zu pflegen, von der jeder erben kann. (Ein Beispiel: Man kann eine Methode nicht so umgestalten, dass sie eine andere öffentliche Methode aufruft, selbst wenn sich das Verhalten der Basisklasse dadurch nicht ändert, weil eine Unterklasse die öffentliche Methode überschrieben haben könnte)
  • El Tödlicher Diamant des Todes

Die Probleme mit der Zusammensetzung

  • Es kann manchmal ein wenig langatmig sein.

Das war's. Ich meine es ernst. Das ist immer noch ein echtes Problem, aber im Allgemeinen ist es nicht so schlimm, zumindest im Vergleich zu den unzähligen Fallstricken, die mit der Vererbung verbunden sind.

Wann sollte die Vererbung eingesetzt werden?

Wenn Sie das nächste Mal Ihre schicken UML-Diagramme für ein Projekt entwerfen (falls Sie das tun) und darüber nachdenken, etwas Vererbung einzubauen, beachten Sie bitte folgenden Rat: Lassen Sie es.

Zumindest noch nicht.

Vererbung wird als Werkzeug zur Erreichung von Polymorphismus verkauft, aber damit verbunden ist ein mächtiges System zur Wiederverwendung von Code, das der meiste Code offen gesagt nicht braucht. Das Problem ist, dass man, sobald man seine Vererbungshierarchie öffentlich macht, auf diese bestimmte Art der Wiederverwendung von Code festgelegt ist, selbst wenn es für die Lösung des eigenen Problems zu viel ist.

Um dies zu vermeiden, würde ich sagen, dass Sie Ihre Basisklassen niemals öffentlich zugänglich machen sollten.

  • Wenn Sie Polymorphismus benötigen, verwenden Sie eine Schnittstelle.
  • Wenn Sie den Benutzern die Möglichkeit geben müssen, das Verhalten Ihrer Klasse anzupassen, sollten Sie explizite Einsprungpunkte über das Strategiemuster Außerdem ist es einfacher, diese Art von API stabil zu halten, da Sie die volle Kontrolle darüber haben, welche Verhaltensweisen sie ändern können und welche nicht.
  • Wenn Sie versuchen, das Prinzip "offen-geschlossen" zu befolgen, indem Sie die Vererbung nutzen, um eine dringend benötigte Aktualisierung einer Klasse zu vermeiden, lassen Sie es einfach. Aktualisieren Sie die Klasse. Ihre Codebasis wird viel sauberer sein, wenn Sie sich tatsächlich um den Code kümmern, für den Sie angestellt sind, anstatt zu versuchen, etwas an die Seite zu tackern. Wenn Sie Angst haben, Fehler einzuführen, dann testen Sie den bestehenden Code.
  • Wenn Sie Code wiederverwenden müssen, sollten Sie zunächst versuchen, Kompositions- oder Hilfsfunktionen zu verwenden.

Wenn Sie beschlossen haben, dass es keine andere gute Option gibt und Sie Vererbung verwenden müssen, um die benötigte Wiederverwendung von Code zu erreichen, dann können Sie sie verwenden, aber beachten Sie die folgenden vier Punkte P.A.I.L. Regeln für die eingeschränkte Vererbung, um sie vernünftig zu halten.

  1. Verwendung der Vererbung als privat Umsetzung im Detail. Legen Sie Ihre Basisklasse nicht öffentlich aus, sondern verwenden Sie dafür Schnittstellen. Auf diese Weise können Sie die Vererbung nach Belieben hinzufügen oder entfernen, ohne dass Sie eine einschneidende Änderung vornehmen müssen.
  2. Behalten Sie Ihre Basisklasse abstrakt . Es macht es einfacher, die Logik, die geteilt werden muss, von der Logik, die nicht geteilt werden muss, zu trennen.
  3. Isolieren Sie Ihre Basis- und Unterklassen. Lassen Sie nicht zu, dass Ihre Unterklasse Methoden der Basisklasse überschreibt (verwenden Sie dafür das Strategiemuster), und vermeiden Sie, dass sie Eigenschaften/Methoden voneinander erwarten, verwenden Sie andere Formen der gemeinsamen Nutzung von Code, um dies zu erreichen.
  4. Vererbung ist eine zuletzt Resort.

El Isolieren Sie Regel mag ein wenig hart klingen, aber wenn Sie sich disziplinieren, werden Sie einige schöne Vorteile haben. Insbesondere gibt sie Ihnen die Freiheit, alle oben erwähnten Fallstricke im Zusammenhang mit der Vererbung zu vermeiden.

  • Sie geben keine Implementierungsdetails über geschützte Mitglieder preis. Sie haben keine Verwendung für protected, wenn Ihre Unterklassen bestrebt sind, die Eigenschaften, die die Basisklasse bietet, nicht zu kennen.
  • Es ist viel einfacher, dem Code zu folgen, weil er sich nicht in und aus Basis- und Unterklassen verzweigt.
  • Das Problem der brüchigen Basis verschwindet. Das Problem der zerbrechlichen Basisklasse rührt daher, dass man Methoden einer Basisklasse beliebig überschreiben kann, ohne dass man es merkt oder sich daran erinnert. Wenn die Vererbung isoliert und privat ist, ist die Basisklasse nicht anfälliger als eine Klasse, die durch Komposition von einer anderen abhängt.
  • Der tödliche Diamant des Todes ist kein Thema mehr, da es einfach keine Notwendigkeit für mehrere Vererbungsschichten gibt. Wenn Sie die abstrakten Basisklassen B und C haben, die beide eine Menge Funktionalität teilen, verschieben Sie diese Funktionalität einfach aus B und C in eine neue abstrakte Basisklasse, Klasse D. Jeder, der von B geerbt hat, sollte aktualisieren, um sowohl von B als auch von D zu erben, und jeder, der von C geerbt hat, sollte von C und D erben. Da Ihre Basisklassen alle private Implementierungsdetails sind, sollte es nicht allzu schwierig sein, herauszufinden, wer von was erbt, um diese Änderungen vorzunehmen.

Schlussfolgerung

Mein erster Vorschlag wäre, in dieser Angelegenheit Ihren Verstand zu benutzen. Viel wichtiger als eine Liste von Regeln für die Verwendung von Vererbung ist ein intuitives Verständnis von Vererbung und den damit verbundenen Vor- und Nachteilen sowie ein gutes Verständnis der anderen Werkzeuge, die anstelle von Vererbung verwendet werden können (Komposition ist nicht die einzige Alternative. Zum Beispiel ist das Strategiemuster ein erstaunliches Werkzeug, das viel zu oft vergessen wird). Wenn Sie ein gutes, solides Verständnis all dieser Werkzeuge haben, werden Sie sich vielleicht dafür entscheiden, Vererbung häufiger zu verwenden, als ich es empfehlen würde, und das ist völlig in Ordnung. Zumindest treffen Sie eine fundierte Entscheidung und verwenden Vererbung nicht nur, weil Sie wissen, wie man es macht.

Lesen Sie weiter:

  • Ein Artikel Ich habe zu diesem Thema einen Artikel geschrieben, der noch tiefer geht und Beispiele liefert.
  • Eine Webpage über drei verschiedene Aufgaben, die Vererbung erfüllt, und wie diese Aufgaben mit anderen Mitteln in der Sprache Go erledigt werden können.
  • Eine Auflistung Gründe, warum es sinnvoll sein kann, eine Klasse als nicht vererbbar zu deklarieren (z. B. "final" in Java).

18voto

simplfuzz Punkte 11783

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 starten
und sie im Handumdrehen zu Drehflügeln umbauen. Das ist im Wesentlichen
ein Motor mit Flügeln. Aber was, wenn ich etwas ändern wollte?
den Motor auch im laufenden Betrieb?

Entweder die Basisklasse Engine stellt einen Mutator zum Ändern seiner
Eigenschaften, oder ich gestalte neu Aircraft als:

Class Aircraft {
  var wings;
  var engine;
}

Jetzt kann ich auch meinen Motor im Handumdrehen austauschen.

0 Stimmen

Ihr Beitrag bringt einen Punkt zur Sprache, den ich zuvor nicht bedacht hatte - um Ihre Analogie zu mechanischen Objekten mit mehreren Teilen fortzusetzen: Bei einer Feuerwaffe gibt es in der Regel ein Teil, das mit einer Seriennummer versehen ist und dessen Seriennummer als die der gesamten Feuerwaffe gilt (bei einer Handfeuerwaffe wäre dies normalerweise der Rahmen). Man kann alle anderen Teile ersetzen und hat immer noch dieselbe Feuerwaffe, aber wenn der Rahmen bricht und ersetzt werden muss, wäre das Ergebnis des Zusammenbaus eines neuen Rahmens mit allen anderen Teilen der ursprünglichen Waffe eine neue Waffe. Beachten Sie, dass...

0 Stimmen

...die Tatsache, dass auf mehreren Teilen einer Waffe Seriennummern angebracht sein können, bedeutet nicht, dass eine Waffe mehrere Identitäten haben kann. Nur die Seriennummer auf dem Rahmen identifiziert die Waffe; die Seriennummer auf allen anderen Teilen identifiziert die Waffe, für die diese Teile hergestellt wurden, was nicht unbedingt die Waffe ist, in die sie zu einem bestimmten Zeitpunkt eingebaut werden.

1 Stimmen

In diesem speziellen Fall von Flugzeugen würde ich definitiv nicht empfehlen, das Triebwerk im laufenden Betrieb zu wechseln.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X