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.

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).

14voto

nabeelfarid Punkte 4026

Sie müssen sich Folgendes ansehen Das Liskovsche Substitutionsprinzip in Onkel Bob's SOLID Grundsätze der Klassengestaltung :)

10voto

Scott Hannen Punkte 24228

Um diese Frage aus einem anderen Blickwinkel für neuere Programmierer zu betrachten:

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

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

Das hört sich gut an, aber in der Praxis funktioniert es fast nie, und zwar aus einem von mehreren Gründen:

  • Wir stellen fest, dass es noch einige andere Funktionen gibt, die unsere Klassen haben sollen. Wenn das Hinzufügen von Funktionen zu Klassen durch Vererbung erfolgt, müssen wir uns entscheiden: Fügen wir sie der bestehenden Basisklasse hinzu, auch wenn nicht jede Klasse, die von ihr erbt, diese Funktionen benötigt? Erstellen wir eine weitere Basisklasse? Aber was ist mit Klassen, die bereits von der anderen Basisklasse erben?
  • Wir stellen fest, dass wir für nur eine der Klassen, die von unserer Basisklasse erben, ein etwas anderes Verhalten der Basisklasse wünschen. Also gehen wir zurück und basteln an unserer Basisklasse herum, fügen vielleicht ein paar virtuelle Methoden hinzu oder, noch schlimmer, einen Code, der besagt: "Wenn ich vom Typ A geerbt habe, mache dies, aber wenn ich vom Typ B geerbt habe, mache das." Das ist aus vielerlei Gründen schlecht. Einer davon ist, dass wir jedes Mal, wenn wir die Basisklasse ändern, effektiv jede geerbte Klasse ändern. Wir ändern also in Wirklichkeit die Klassen A, B, C und D, weil wir ein etwas anderes Verhalten in Klasse A brauchen. So vorsichtig wir auch sein mögen, wir könnten eine dieser Klassen aus Gründen, die nichts mit diesen Klassen zu tun haben, zerstören.
  • Wir wissen vielleicht, warum wir uns dafür entschieden haben, alle diese Klassen voneinander erben zu lassen, aber für jemand anderen, der unseren Code pflegen muss, macht es vielleicht keinen Sinn (wahrscheinlich nicht). Wir könnten sie vor eine schwierige Entscheidung stellen: Mache ich etwas wirklich Hässliches und Unordentliches, um die benötigte Änderung vorzunehmen (siehe den vorigen Punkt), oder schreibe ich einfach einen Haufen davon neu.

Am Ende verwickeln wir unseren Code in schwierige Knoten und haben keinerlei Nutzen davon, außer dass wir sagen können: "Cool, ich habe etwas über Vererbung gelernt und sie jetzt angewendet." Das soll nicht herablassend klingen, denn wir haben es alle schon getan. Aber wir haben es alle getan, weil uns niemand gesagt hat, dass wir es nicht tun sollen.

Sobald mir jemand erklärt hat, dass ich Komposition der Vererbung vorziehen soll, habe ich mich daran erinnert, dass ich jedes Mal versucht habe, Funktionen zwischen Klassen mit Hilfe von Vererbung zu teilen, und mir ist klar geworden, dass das meistens nicht gut funktioniert hat.

Das Gegenmittel ist die Prinzip der einzigen Verantwortung . Betrachten Sie es als eine Einschränkung. Meine Klasse muss eine Sache tun. I muss in der Lage sein, meiner Klasse einen Namen zu geben, der diese eine Sache, die sie tut, irgendwie beschreibt. (Es gibt für alles Ausnahmen, aber absolute Regeln sind manchmal besser, wenn wir etwas lernen wollen). Daraus folgt, dass ich keine Basisklasse schreiben kann, die ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses . Was auch immer an Funktionalität benötigt wird, muss in einer eigenen Klasse untergebracht werden, und andere Klassen, die diese Funktionalität benötigen, können von dieser Klasse abhängen, ノット davon erben.

Auf die Gefahr hin, es zu sehr zu vereinfachen: Das ist Komposition - das Zusammenstellen mehrerer Klassen, damit sie zusammenarbeiten. Und wenn wir uns das erst einmal angewöhnt haben, stellen wir fest, dass es viel flexibler, wartbarer und testbarer ist als die Vererbung.

0 Stimmen

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

0 Stimmen

In der Zeit, seit ich diese Antwort geschrieben habe, habe ich gelesen diese Stelle von "Onkel Bob", der diesen Mangel an Fähigkeiten behebt. Ich habe noch nie eine Sprache verwendet, die Mehrfachvererbung erlaubt. Aber wenn ich zurückblicke, ist die Frage als "sprachenunabhängig" gekennzeichnet und meine Antwort geht von C# aus. Ich muss meinen Horizont erweitern.

8voto

Anzurio Punkte 16224

Wenn Sie die API der Basisklasse "kopieren"/offenlegen wollen, verwenden Sie Vererbung. Wenn Sie nur die Funktionalität "kopieren" wollen, verwenden Sie die Delegation.

Ein Beispiel dafür: Sie möchten aus einer Liste einen Stapel erstellen. Stack hat nur pop, push und peek. Sie sollten keine Vererbung verwenden, da Sie in einem Stack keine Funktionen wie push_back, push_front, removeAt usw. benötigen.

0 Stimmen

@Anzurio....Wie unterscheiden Sie zwischen API und Funktionsfähigkeit? Meiner Meinung nach werden exponierte Methoden einer Klasse als API-Methoden dieser Klasse bezeichnet. Diese sind auch die Funktionalität der Klasse. Wenn Sie Komposition verwenden, verwenden Sie öffentliche Methoden der Klasse, wir haben sie bereits API genannt.

1 Stimmen

@sdindiver Entschuldigung für die Verwirrung. Worauf ich hinauswollte, ist, dass man bei der Vererbung die komplette API der Elternklasse offenlegt, aber wenn die Kindklasse nicht die komplette API der Elternklasse offenlegen muss, verwendet man stattdessen die Komposition, so dass die Kindklasse Zugriff auf die Funktionalität der Elternklasse hat, ohne deren komplette API offenzulegen.

7voto

Tero Tolonen Punkte 3889

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

Die Komposition ist eine modulare Vorgehensweise: Sie erstellen eine Schnittstelle, die der übergeordneten Klasse ähnelt, erstellen ein neues Objekt und delegieren Aufrufe an dieses. Wenn diese Objekte nichts voneinander wissen müssen, ist es ziemlich sicher und einfach, Komposition zu verwenden. Es gibt hier so viele Möglichkeiten.

Wenn die Elternklasse jedoch aus irgendeinem Grund auf Funktionen zugreifen muss, die von der "Kindklasse" bereitgestellt werden, kann es für unerfahrene Programmierer so aussehen, als sei dies ein großartiger Ort, um Vererbung zu nutzen. Die Elternklasse kann einfach ihr eigenes abstraktes "foo()" aufrufen, das von der Unterklasse überschrieben wird, und dann den Wert an die abstrakte Basisklasse weitergeben.

Es sieht nach einer netten Idee aus, aber in vielen Fällen ist es besser, der Klasse ein Objekt zu geben, das foo() implementiert (oder sogar den Wert von foo() manuell zu setzen), als die neue Klasse von einer Basisklasse zu erben, die die Angabe der Funktion foo() erfordert.

Warum?

Weil die Vererbung ein schlechter Weg ist, Informationen zu übertragen .

Die Komposition hat hier einen echten Vorteil: die Beziehung kann umgekehrt werden: die "Elternklasse" oder der "abstrakte Arbeiter" kann alle spezifischen "Kind"-Objekte, die eine bestimmte Schnittstelle implementieren, zusammenfassen + jedes untergeordnete Element kann in jedes andere übergeordnete Element gesetzt werden, das seinen Typ annimmt . Und es kann eine beliebige Anzahl von Objekten geben, z. B. MergeSort oder QuickSort könnten eine beliebige Liste von Objekten sortieren, die eine abstrakte Vergleichsschnittstelle implementieren. Oder anders ausgedrückt: eine beliebige Gruppe von Objekten, die "foo()" implementieren, und eine andere Gruppe von Objekten, die von Objekten mit "foo()" Gebrauch machen können, können zusammen spielen.

Mir fallen drei echte Gründe für die Verwendung der Vererbung ein:

  1. Sie haben viele Klassen mit dieselbe Schnittstelle und Sie wollen Zeit sparen, indem Sie sie schreiben
  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, ist es wahrscheinlich notwendig, Vererbung zu verwenden.

Es ist nichts Schlechtes daran, Grund 1 zu verwenden, es ist eine sehr gute Sache, eine solide Schnittstelle für Ihre Objekte zu haben. Dies kann durch Komposition oder durch Vererbung geschehen, kein Problem - wenn diese Schnittstelle einfach ist und sich nicht ändert. Normalerweise ist Vererbung hier sehr effektiv.

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

Wenn Sie jedoch die privaten Variablen, den Fall 3, verwenden wollen, können Sie in Schwierigkeiten geraten. Wenn Sie globale Variablen für unsicher halten, dann sollten Sie die Vererbung nutzen, um Zugriff auf private Variablen zu erhalten, die ebenfalls unsicher sind . Allerdings sind globale Variablen gar nicht so schlecht - Datenbanken sind im Wesentlichen eine große Menge globaler Variablen. Aber wenn Sie damit umgehen können, dann ist das schon in Ordnung.

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