Vererbung vs. Aggregation [geschlossen]


151

Es gibt zwei Denkrichtungen, wie Code in einem objektorientierten System am besten erweitert, verbessert und wiederverwendet werden kann:

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

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


9
Schöne Frage, leider habe ich jetzt nicht genug Zeit.
Toon Krijthe

4
Eine gute Antwort ist besser als eine schnellere ... Ich werde meine eigene Frage beobachten, also werde ich Sie mindestens abstimmen :-P
Craig Walker

Antworten:


181

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.

  • Wenn die neue Klasse ist mehr oder weniger wie die ursprüngliche Klasse. Vererbung verwenden. Die neue Klasse ist jetzt eine Unterklasse der ursprünglichen Klasse.
  • Wenn die neue Klasse die ursprüngliche Klasse haben muss . Verwenden Sie die Aggregation. Die neue Klasse hat jetzt die ursprüngliche Klasse als Mitglied.

Es gibt jedoch eine große Grauzone. Wir brauchen also noch einige andere Tricks.

  • Wenn wir Vererbung verwendet haben (oder planen, sie zu verwenden), aber nur einen Teil der Schnittstelle verwenden, oder wenn wir gezwungen sind, viele Funktionen zu überschreiben, um die Korrelation logisch zu halten. Dann haben wir einen großen üblen Geruch, der darauf hinweist, dass wir Aggregation verwenden mussten.
  • Wenn wir die Aggregation verwendet haben (oder planen, sie zu verwenden), aber feststellen, dass wir fast die gesamte Funktionalität kopieren müssen. Dann haben wir einen Geruch, der in Richtung Vererbung zeigt.

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

class Dog
  Eat;
  Walk;
  Bark;
  Play;
end;

Wir brauchen jetzt eine Klasse 'Katze', die 'Essen', 'Gehen', 'Schnurren' und 'Spielen' braucht. Versuchen Sie also zuerst, es von einem Hund zu verlängern.

class Cat is Dog
  Purr; 
end;

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.

class Cat is Dog
  Purr; 
  Bark = null;
end;

Ok, das funktioniert, aber es riecht schlecht. Versuchen wir also eine Aggregation:

class Cat
  has Dog;
  Eat = Dog.Eat;
  Walk = Dog.Walk;
  Play = Dog.Play;
  Purr;
end;

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:

class Pet
  Eat;
  Walk;
  Play;
end;

class Dog is Pet
  Bark;
end;

class Cat is Pet
  Purr;
end;

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.


5
Die "Wiederverwendung fast aller Funktionen einer Klasse" ist das einzige Mal, dass ich die Vererbung tatsächlich bevorzuge. Was ich wirklich gerne hätte, ist eine Sprache, die leicht sagen kann: "Für diese spezifischen Methoden an dieses aggregierte Objekt delegieren". Das ist das Beste aus beiden Welten.
Craig Walker

Bei anderen Sprachen bin ich mir nicht sicher, aber Delphi verfügt über einen Mechanismus, mit dem Mitglieder einen Teil der Benutzeroberfläche implementieren können.
Toon Krijthe

2
Ziel C verwendet Protokolle, mit denen Sie entscheiden können, ob Ihre Funktion erforderlich oder optional ist.
cantfindaname88

1
Tolle Antwort mit einem praktischen Beispiel. Ich würde die Pet-Klasse mit einer Methode namens makeSoundaufrufen und jede Unterklasse ihre eigene Art von Sound implementieren lassen. Dies hilft dann in Situationen, in denen Sie viele Haustiere haben und dies einfach tun for_each pet in the list of pets, pet.makeSound.
Blongho

38

Zu Beginn der GOF geben sie an

Bevorzugen Sie die Objektzusammensetzung gegenüber der Klassenvererbung.

Dies wird weiter diskutiert hier


27

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.


Der Unterschied lässt sich auch gut in der Frage zusammenfassen, auf die Sie antworten sollen. Ich möchte, dass die Leute zuerst Fragen lesen, bevor sie mit dem Übersetzen beginnen. Fördern Sie blind Designmuster, um Mitglied des Elite-Clubs zu werden?
Val

Ich mag diesen Ansatz nicht, es geht mehr um Komposition
Alireza Rahmani Khalili

15

Hier ist mein häufigstes Argument:

In jedem objektorientierten System besteht jede Klasse aus zwei Teilen:

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

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


Ich würde denken, dass Sie eine abstrakte Klasse verwenden, um eine Schnittstelle zu definieren, und dann die direkte Vererbung für jede konkrete Implementierungsklasse verwenden (was sie ziemlich flach macht). Jede Aggregation erfolgt unter konkreter Umsetzung.
Orcmid

Wenn Sie zusätzliche Schnittstellen benötigen, geben Sie diese über Methoden auf der Schnittstelle der abstrahierten Schnittstelle der Hauptebene zurück und wiederholen Sie den Vorgang.
Orcmid

Denken Sie, dass die Aggregation in diesem Szenario besser ist? Ein Buch ist ein SellingItem, ist A DigitalDisc ein SelliingItem codereview.stackexchange.com/questions/14077/...
LCJ

11

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.


6

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:

  1. Ihre Wahl wird wahrscheinlich irgendwann die falsche sein
  2. Diese Wahl zu ändern ist schwierig, wenn Sie sie einmal getroffen haben.
  3. Vererbung ist in der Regel eine schlechtere Wahl, da sie einschränkender ist.

Daher tendiere ich dazu, Aggregation zu wählen - auch wenn es eine starke Beziehung zu sein scheint.


Sich ändernde Anforderungen bedeuten, dass Ihr OO-Design unweigerlich falsch ist. Vererbung vs. Aggregation ist nur die Spitze dieses Eisbergs. Sie können nicht für alle möglichen Zukünfte planen. Entscheiden Sie sich einfach für die XP-Idee: Lösen Sie die heutigen Anforderungen und akzeptieren Sie, dass Sie möglicherweise umgestalten müssen.
Bill Karwin

Ich mag diese Art der Untersuchung und Befragung. Besorgt über Unschärfe ist-a, hat-a und verwendet-a obwohl. Ich beginne mit Schnittstellen (zusammen mit der Identifizierung der implementierten Abstraktionen, zu denen Schnittstellen gehören sollen). Die zusätzliche Indirektionsebene ist von unschätzbarem Wert. Folgen Sie nicht Ihrer Aggregationsschlussfolgerung.
Orcmid

Das ist aber so ziemlich mein Punkt. Sowohl Vererbung als auch Aggregation sind Methoden zur Lösung des Problems. Eine dieser Methoden führt zu allen möglichen Strafen, wenn Sie morgen Probleme lösen müssen.
Craig Walker

In meinen Augen sind Aggregation + Schnittstellen die Alternative zu Vererbung / Unterklasse. Sie können Polymorphismus nicht allein auf der Grundlage der Aggregation durchführen.
Craig Walker

3

Die Frage wird normalerweise als Zusammensetzung vs. Vererbung formuliert und wurde hier bereits gestellt.


Richtig, du bist ... lustig, dass SO das nicht als eine der verwandten Fragen angegeben hat.
Craig Walker

2
SO Suche saugt immer noch große Zeit
Vinko Vrsalovic

Einverstanden. Deshalb habe ich das nicht geschlossen. Das und viele Leute hatten hier bereits geantwortet.
Bill the Lizard

1
SO benötigt eine Funktion, um 2 Fragen (und ihre Antworten) in 1
Craig Walker

3

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


2

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.


2

Ich denke, es ist keine Entweder-Oder-Debatte. Es ist nur so dass:

  1. is-a-Beziehungen (Vererbungsbeziehungen) treten seltener auf als has-a-Beziehungen (Zusammensetzungsbeziehungen).
  2. Vererbung ist schwieriger zu korrigieren, selbst wenn es angemessen ist, sie zu verwenden. Daher muss eine sorgfältige Prüfung durchgeführt werden, da sie die Kapselung unterbrechen, eine enge Kopplung fördern kann, indem die Implementierung offengelegt wird, und so weiter.

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.


1

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


0

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.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.