Es gibt zwei Denkrichtungen, wie Code in einem objektorientierten System am besten erweitert, verbessert und wiederverwendet werden kann:
Vererbung: Erweitern Sie die Funktionalität einer Klasse, indem Sie eine Unterklasse erstellen. Überschreiben Sie Superklassenmitglieder in den Unterklassen, um neue Funktionen bereitzustellen. Machen Sie Methoden abstrakt / virtuell, um Unterklassen zum "Ausfüllen der Lücken" zu zwingen, wenn die Oberklasse eine bestimmte Schnittstelle möchte, aber hinsichtlich ihrer Implementierung agnostisch ist.
Aggregation: Erstellen Sie neue Funktionen, indem Sie andere Klassen nehmen und zu einer neuen Klasse kombinieren. Fügen Sie dieser neuen Klasse eine gemeinsame Schnittstelle hinzu, um die Interoperabilität mit anderem Code zu gewährleisten.
Was sind die Vorteile, Kosten und Konsequenzen von jedem? Gibt es andere Alternativen?
Ich sehe, dass diese Debatte regelmäßig stattfindet, aber ich glaube nicht, dass sie noch zum Stapelüberlauf gestellt wurde (obwohl es einige verwandte Diskussionen gibt). Es gibt auch einen überraschenden Mangel an guten Google-Ergebnissen.
quelle
Antworten:
Es geht nicht darum, welches das Beste ist, sondern darum, wann was zu verwenden ist.
In den "normalen" Fällen reicht eine einfache Frage aus, um herauszufinden, ob wir Vererbung oder Aggregation benötigen.
Es gibt jedoch eine große Grauzone. Wir brauchen also noch einige andere Tricks.
Um es kurz zu machen. Wir sollten die Aggregation verwenden, wenn ein Teil der Schnittstelle nicht verwendet wird oder geändert werden muss, um eine unlogische Situation zu vermeiden. Wir müssen die Vererbung nur verwenden, wenn wir fast die gesamte Funktionalität ohne größere Änderungen benötigen. Verwenden Sie im Zweifelsfall Aggregation.
Eine andere Möglichkeit für den Fall, dass wir eine Klasse haben, die einen Teil der Funktionalität der ursprünglichen Klasse benötigt, besteht darin, die ursprüngliche Klasse in eine Stammklasse und eine Unterklasse aufzuteilen. Und lassen Sie die neue Klasse von der Stammklasse erben. Aber Sie sollten darauf achten, keine unlogische Trennung zu schaffen.
Fügen wir ein Beispiel hinzu. Wir haben eine Klasse 'Hund' mit Methoden: 'Essen', 'Gehen', 'Bellen', 'Spielen'.
Wir brauchen jetzt eine Klasse 'Katze', die 'Essen', 'Gehen', 'Schnurren' und 'Spielen' braucht. Versuchen Sie also zuerst, es von einem Hund zu verlängern.
Sieht gut aus, aber warte. Diese Katze kann bellen (Katzenliebhaber werden mich dafür töten). Und eine bellende Katze verstößt gegen die Prinzipien des Universums. Wir müssen also die Bark-Methode überschreiben, damit sie nichts bewirkt.
Ok, das funktioniert, aber es riecht schlecht. Versuchen wir also eine Aggregation:
Ok, das ist schön. Diese Katze bellt nicht mehr, nicht einmal still. Aber es hat immer noch einen internen Hund, der raus will. Versuchen wir also Lösung Nummer drei:
Das ist viel sauberer. Keine internen Hunde. Und Katzen und Hunde sind auf dem gleichen Niveau. Wir können sogar andere Haustiere vorstellen, um das Modell zu erweitern. Es sei denn, es ist ein Fisch oder etwas, das nicht läuft. In diesem Fall müssen wir erneut umgestalten. Aber das ist etwas für eine andere Zeit.
quelle
makeSound
aufrufen und jede Unterklasse ihre eigene Art von Sound implementieren lassen. Dies hilft dann in Situationen, in denen Sie viele Haustiere haben und dies einfach tunfor_each pet in the list of pets, pet.makeSound
.Zu Beginn der GOF geben sie an
Dies wird weiter diskutiert hier
quelle
Der Unterschied wird typischerweise als der Unterschied zwischen "ist a" und "hat a" ausgedrückt. Vererbung, die "ist eine" Beziehung, wird im Liskov-Substitutionsprinzip gut zusammengefasst . Aggregation, „hat eine“ Beziehung, ist genau das - es zeigt , dass das Aggregieren Objekt hat eine der aggregierten Objekte.
Es gibt auch weitere Unterschiede - die private Vererbung in C ++ zeigt an, dass eine Beziehung "in Bezug auf implementiert" ist, die auch durch die Aggregation von (nicht exponierten) Mitgliedsobjekten modelliert werden kann.
quelle
Hier ist mein häufigstes Argument:
In jedem objektorientierten System besteht jede Klasse aus zwei Teilen:
Seine Schnittstelle : das "öffentliche Gesicht" des Objekts. Dies sind die Funktionen, die dem Rest der Welt angekündigt werden. In vielen Sprachen ist die Menge gut als "Klasse" definiert. Normalerweise sind dies die Methodensignaturen des Objekts, obwohl diese je nach Sprache etwas variieren.
Seine Implementierung : Die Arbeit "hinter den Kulissen", die das Objekt tut, um seine Schnittstelle zu erfüllen und Funktionalität bereitzustellen. Dies sind normalerweise die Code- und Mitgliedsdaten des Objekts.
Eines der Grundprinzipien von OOP ist, dass die Implementierung innerhalb der Klasse gekapselt (dh verborgen) ist. Das einzige, was Außenstehende sehen sollten, ist die Benutzeroberfläche.
Wenn eine Unterklasse von einer Unterklasse erbt, erbt sie normalerweise sowohl die Implementierung als auch die Schnittstelle. Dies bedeutet wiederum, dass Sie gezwungen sind, beide als Einschränkungen für Ihre Klasse zu akzeptieren.
Bei der Aggregation können Sie entweder die Implementierung oder die Schnittstelle oder beides auswählen - aber Sie werden nicht dazu gezwungen. Die Funktionalität eines Objekts bleibt dem Objekt selbst überlassen. Es kann sich nach Belieben auf andere Objekte verschieben, ist aber letztendlich für sich selbst verantwortlich. Nach meiner Erfahrung führt dies zu einem flexibleren System, das einfacher zu ändern ist.
Wenn ich objektorientierte Software entwickle, bevorzuge ich fast immer die Aggregation gegenüber der Vererbung.
quelle
Ich gab eine Antwort auf "Ist ein" vs "Hat ein": Welches ist besser? .
Grundsätzlich stimme ich anderen Leuten zu: Verwenden Sie die Vererbung nur, wenn Ihre abgeleitete Klasse wirklich der Typ ist, den Sie erweitern, und nicht nur, weil sie dieselben Daten enthält . Denken Sie daran, dass Vererbung bedeutet, dass die Unterklasse sowohl die Methoden als auch die Daten erhält.
Ist es für Ihre abgeleitete Klasse sinnvoll, alle Methoden der Oberklasse zu haben? Oder versprechen Sie sich nur leise, dass diese Methoden in der abgeleiteten Klasse ignoriert werden sollten? Oder überschreiben Sie Methoden aus der Oberklasse und machen sie zu No-Ops, sodass niemand sie versehentlich aufruft? Oder geben Sie Ihrem API-Tool zur Dokumentgenerierung Hinweise, um die Methode aus dem Dokument zu entfernen?
Dies sind starke Hinweise darauf, dass Aggregation in diesem Fall die bessere Wahl ist.
quelle
Ich sehe viele "is-a vs. has-a; sie sind konzeptionell unterschiedlich" Antworten auf diese und die damit verbundenen Fragen.
Das einzige, was ich in meiner Erfahrung gefunden habe, ist, dass der Versuch, festzustellen, ob eine Beziehung "is-a" oder "has-a" ist, scheitern wird. Selbst wenn Sie diese Bestimmung jetzt für die Objekte korrekt vornehmen können, bedeuten sich ändernde Anforderungen, dass Sie wahrscheinlich irgendwann in der Zukunft falsch liegen werden.
Eine andere Sache, die ich gefunden habe, ist, dass es sehr schwierig ist, von Vererbung zu Aggregation zu konvertieren, wenn viel Code um eine Vererbungshierarchie geschrieben ist. Nur von einer Oberklasse zu einer Schnittstelle zu wechseln, bedeutet, fast jede Unterklasse im System zu ändern.
Und wie ich an anderer Stelle in diesem Beitrag erwähnt habe, ist die Aggregation tendenziell weniger flexibel als die Vererbung.
Sie haben also einen perfekten Sturm von Argumenten gegen die Vererbung, wenn Sie sich für das eine oder andere entscheiden müssen:
Daher tendiere ich dazu, Aggregation zu wählen - auch wenn es eine starke Beziehung zu sein scheint.
quelle
Die Frage wird normalerweise als Zusammensetzung vs. Vererbung formuliert und wurde hier bereits gestellt.
quelle
Ich wollte dies zu einem Kommentar zur ursprünglichen Frage machen, aber 300 Zeichen beißen [; <).
Ich denke, wir müssen vorsichtig sein. Erstens gibt es mehr Geschmacksrichtungen als die beiden eher spezifischen Beispiele in der Frage.
Ich schlage auch vor, dass es wertvoll ist, das Ziel nicht mit dem Instrument zu verwechseln. Man möchte sicherstellen, dass die gewählte Technik oder Methodik das Erreichen des primären Ziels unterstützt, aber ich denke nicht, dass eine Diskussion außerhalb des Kontexts, welche Technik die beste ist, sehr nützlich ist. Es hilft, die Fallstricke der verschiedenen Ansätze zusammen mit ihren klaren Sweet Spots zu kennen.
Was möchten Sie beispielsweise erreichen, was steht Ihnen zunächst zur Verfügung und welche Einschränkungen bestehen?
Erstellen Sie ein Komponenten-Framework, auch ein spezielles? Sind Schnittstellen von Implementierungen im Programmiersystem trennbar oder wird dies durch eine Praxis erreicht, die eine andere Art von Technologie verwendet? Können Sie die Vererbungsstruktur von Schnittstellen (falls vorhanden) von der Vererbungsstruktur von Klassen trennen, die sie implementieren? Ist es wichtig, die Klassenstruktur einer Implementierung vor dem Code zu verbergen, der auf den von der Implementierung bereitgestellten Schnittstellen basiert? Gibt es mehrere Implementierungen, die gleichzeitig verwendet werden können, oder ist die Variation im Laufe der Zeit aufgrund von Wartung und Verbesserung stärker? Dies und mehr müssen berücksichtigt werden, bevor Sie sich auf ein Tool oder eine Methodik konzentrieren.
Ist es schließlich so wichtig, Unterscheidungen in der Abstraktion zu sperren und wie Sie es (wie in is-a versus has-a) für verschiedene Merkmale der OO-Technologie halten? Vielleicht, wenn dadurch die konzeptionelle Struktur für Sie und andere konsistent und überschaubar bleibt. Aber es ist ratsam, sich davon und den Verzerrungen, die Sie möglicherweise machen, nicht versklaven zu lassen. Vielleicht ist es am besten, eine Ebene zurückzutreten und nicht so starr zu sein (aber eine gute Erzählung zu hinterlassen, damit andere sagen können, was los ist). [Ich suche nach dem, was einen bestimmten Teil eines Programms erklärbar macht, aber manchmal strebe ich nach Eleganz, wenn es einen größeren Gewinn gibt. Nicht immer die beste Idee.]
Ich bin ein Schnittstellenpurist und ich bin von den Arten von Problemen und Ansätzen angezogen, bei denen Schnittstellenpurismus angemessen ist, sei es beim Erstellen eines Java-Frameworks oder beim Organisieren einiger COM-Implementierungen. Das macht es nicht für alles angemessen, nicht einmal für alles, obwohl ich es schwöre. (Ich habe einige Projekte, die ernsthafte Gegenbeispiele gegen den Schnittstellenpurismus liefern, daher wird es interessant sein zu sehen, wie ich damit zurechtkomme.)
quelle
Ich werde den Teil behandeln, wo diese zutreffen könnten. Hier ist ein Beispiel für beides in einem Spielszenario. Angenommen, es gibt ein Spiel mit verschiedenen Arten von Soldaten. Jeder Soldat kann einen Rucksack haben, der verschiedene Dinge aufnehmen kann.
Vererbung hier? Es gibt eine Marine, eine grüne Baskenmütze und einen Scharfschützen. Dies sind Arten von Soldaten. Es gibt also einen Soldaten der Basisklasse mit Marine, Green Beret & Sniper als abgeleiteten Klassen
Aggregation hier? Der Rucksack kann Granaten, Gewehre (verschiedene Typen), Messer, Medikit usw. enthalten. Ein Soldat kann zu jedem Zeitpunkt mit diesen ausgestattet werden. Außerdem kann er eine kugelsichere Weste haben, die bei Angriffen als Rüstung dient Die Verletzung nimmt auf einen bestimmten Prozentsatz ab. Die Soldatenklasse enthält ein Objekt der kugelsicheren Weste und die Rucksackklasse, die Verweise auf diese Gegenstände enthält.
quelle
Ich denke, es ist keine Entweder-Oder-Debatte. Es ist nur so dass:
Beide haben ihren Platz, aber die Vererbung ist riskanter.
Obwohl es natürlich keinen Sinn machen würde, eine Klassenform mit einem Punkt und einem Quadrat zu haben. Hier ist die Vererbung fällig.
Menschen neigen dazu, zuerst über Vererbung nachzudenken, wenn sie versuchen, etwas Erweiterbares zu entwerfen, das ist falsch.
quelle
Gunst entsteht, wenn sich beide Kandidaten qualifizieren. A und B sind Optionen, und Sie bevorzugen A. Der Grund dafür ist, dass die Komposition mehr Erweiterungs- / Flexibilitätsmöglichkeiten bietet als die Verallgemeinerung. Diese Erweiterung / Flexibilität bezieht sich hauptsächlich auf die Laufzeit / dynamische Flexibilität.
Der Nutzen ist nicht sofort sichtbar. Um den Nutzen zu sehen, müssen Sie auf die nächste unerwartete Änderungsanforderung warten. In den meisten Fällen scheitern diejenigen, die an der Generalisierung festhalten, im Vergleich zu denen, die sich der Komposition verschrieben haben (mit Ausnahme eines offensichtlichen Falls, der später erwähnt wird). Daher die Regel. Wenn Sie aus Lernsicht eine Abhängigkeitsinjektion erfolgreich implementieren können, sollten Sie wissen, welche Sie wann bevorzugen sollten. Die Regel hilft Ihnen auch bei der Entscheidungsfindung. Wenn Sie sich nicht sicher sind, wählen Sie Komposition.
Zusammenfassung: Zusammensetzung: Die Kopplung wird reduziert, indem nur einige kleinere Dinge in etwas Größeres eingesteckt werden, und das größere Objekt ruft nur das kleinere Objekt zurück. Generierung: Aus API-Sicht ist die Definition, dass eine Methode überschrieben werden kann, eine stärkere Verpflichtung als die Definition, dass eine Methode aufgerufen werden kann. (sehr wenige Fälle, in denen die Generalisierung gewinnt). Und vergessen Sie niemals, dass Sie bei der Komposition auch die Vererbung von einer Schnittstelle anstelle einer großen Klasse verwenden
quelle
Beide Ansätze werden verwendet, um unterschiedliche Probleme zu lösen. Sie müssen nicht immer über zwei oder mehr Klassen aggregieren, wenn Sie von einer Klasse erben.
Manchmal müssen Sie eine einzelne Klasse aggregieren, weil diese Klasse versiegelt ist oder andere nicht virtuelle Mitglieder hat, die Sie abfangen müssen, damit Sie eine Proxy-Schicht erstellen, die offensichtlich nicht in Bezug auf die Vererbung gültig ist, sondern solange die Klasse, die Sie vertreten hat eine Schnittstelle, die Sie abonnieren können, die ziemlich gut funktionieren kann.
quelle