Warum sollte ich Kompositionen der Vererbung vorziehen?


109

Ich habe immer gelesen, dass Kompositionen der Vererbung vorzuziehen sind. Ein Blogbeitrag über andere Arten zum Beispiel befürwortet die Verwendung von Komposition anstelle von Vererbung, aber ich kann nicht sehen, wie Polymorphismus erreicht wird.

Aber ich habe das Gefühl, wenn Leute sagen, sie bevorzugen Komposition, dann meinen sie wirklich, sie bevorzugen eine Kombination aus Komposition und Schnittstellenimplementierung. Wie wirst du Polymorphismus ohne Vererbung bekommen?

Hier ist ein konkretes Beispiel, in dem ich Vererbung verwende. Wie würde dies geändert, um Komposition zu verwenden, und was würde ich gewinnen?

Class Shape
{
    string name;
  public:
    void getName();
    virtual void draw()=0;
}

Class Circle: public Shape
{
    void draw(/*draw circle*/);
}

57
Nein, wenn Leute sagen , bevorzugen Zusammensetzung sie wirklich bedeuten bevorzugen Zusammensetzung, nicht nie jemals Vererbung . Ihre ganze Frage basiert auf einer fehlerhaften Prämisse. Verwenden Sie die Vererbung, wenn es angebracht ist.
Bill the Lizard

2
Vielleicht möchten Sie auch einen Blick auf programmers.stackexchange.com/questions/65179/…
vjones

2
Ich stimme dem Gesetzesentwurf zu und sehe die Verwendung der Vererbung als gängige Praxis bei der GUI-Entwicklung an.
Prashant Cholachagudda

2
Sie möchten die Komposition verwenden, da ein Quadrat nur die Komposition von zwei Dreiecken ist. Tatsächlich denke ich, dass alle Formen außer Ellipsen nur Kompositionen von Dreiecken sind. Bei Polymorphismus geht es um vertragliche Verpflichtungen, die zu 100% aus der Vererbung genommen werden. Wenn das Dreieck eine Reihe von Kuriositäten erhält, die hinzugefügt werden, weil jemand Pyramiden erzeugen wollte. Wenn Sie von Triangle erben, erhalten Sie all das, obwohl Sie niemals eine 3D-Pyramide aus Ihrem Sechseck erzeugen werden .
Jimmy Hoffa

2
@BilltheLizard Ich denke, viele Leute, die sagen, dass es wirklich so ist, dass sie niemals Vererbung verwenden, aber sie liegen falsch.
immibis

Antworten:


51

Polymorphismus bedeutet nicht unbedingt Vererbung. Vererbung wird häufig als einfaches Mittel zum Implementieren von polymorphem Verhalten verwendet, da es praktisch ist, Objekte mit ähnlichem Verhalten so zu klassifizieren, dass sie eine vollständig gemeinsame Stammstruktur und ein gemeinsames Verhalten aufweisen. Denken Sie an all die Auto- und Hundebeispiele, die Sie im Laufe der Jahre gesehen haben.

Aber was ist mit Objekten, die nicht gleich sind? Das Modellieren eines Autos und eines Planeten wäre sehr unterschiedlich, und dennoch möchten beide möglicherweise das Move () - Verhalten implementieren.

Eigentlich haben Sie Ihre eigene Frage beantwortet, als Sie sagten "But I have a feeling that when people say prefer composition, they really mean prefer a combination of composition and interface implementation.". Gemeinsames Verhalten kann über Schnittstellen und eine Verhaltenszusammensetzung bereitgestellt werden.

Was das Bessere ist, ist die Antwort etwas subjektiv und hängt davon ab, wie Ihr System funktionieren soll, was sowohl inhaltlich als auch architektonisch sinnvoll ist und wie einfach es zu testen und zu warten ist.


In der Praxis wird angegeben, wie oft "Polymorphismus über Schnittstelle" auftritt und ob dies als normal angesehen wird (im Gegensatz zur Ausnutzung der Ausdruckskraft einer Sprache). Ich würde wetten, dass der Polymorphismus durch Vererbung durch sorgfältiges Design zustande gekommen ist und keine Konsequenz der Sprache (C ++), die nach ihrer Spezifikation entdeckt wurde.
Samis

1
Wer würde move () auf einem Planeten und einem Auto nennen und es als dasselbe betrachten?! Die Frage ist hier, in welchem ​​Kontext sie sich bewegen können sollten. Wenn beide in einem einfachen 2D-Spiel 2D-Objekte sind, können sie den Zug erben. Wenn es sich um Objekte in einer massiven Datensimulation handelt, ist es möglicherweise nicht sinnvoll, sie von derselben Basis erben zu lassen. Infolgedessen müssen Sie eine verwenden Schnittstelle
NikkyD

2
@SamusArin Es zeigt nach oben überall und ist völlig normal in Sprachen berücksichtigt , die Schnittstellen unterstützen. Was meinen Sie mit "einer Ausbeutung der Ausdruckskraft einer Sprache"? Dies ist , was Schnittstellen gibt es für .
Andres F.

@AndresF. Ich bin gerade in einem Beispiel auf "Polymorphismus über Schnittstelle" gestoßen, als ich "Objektorientierte Analyse und Design mit Anwendungen" gelesen habe, die das Beobachtermuster demonstrierten. Ich habe dann meine Antwort realisiert.
Samis

@AndresF. Ich schätze, meine Vision war diesbezüglich etwas verblendet, weil ich in meinem letzten (ersten) Projekt Polymorphismus verwendet habe. Ich hatte 5 Datensatztypen, die alle von derselben Basis abgeleitet waren. Trotzdem danke für die Aufklärung.
Samis

79

Komposition zu bevorzugen bedeutet nicht nur Polymorphismus. Obwohl das ein Teil davon ist und Sie Recht haben, ist (zumindest in nominal getippten Sprachen), was die Leute wirklich meinen, "eine Kombination aus Komposition und Schnittstellenimplementierung zu bevorzugen". Die Gründe für die Bevorzugung der Komposition (unter vielen Umständen) sind jedoch tiefgreifend.

Beim Polymorphismus geht es um eine Sache, die sich auf verschiedene Arten verhält. Generika / Vorlagen sind insofern ein "polymorphes" Merkmal, als sie es einem einzelnen Codeteil ermöglichen, sein Verhalten mit Typen zu variieren. Tatsächlich verhält sich diese Art von Polymorphismus am besten und wird allgemein als parametrischer Polymorphismus bezeichnet, da die Variation durch einen Parameter definiert wird.

Viele Sprachen bieten eine Form von Polymorphismus, die als "Überladung" oder Ad-hoc-Polymorphismus bezeichnet wird, wobei mehrere gleichnamige Prozeduren ad hoc definiert werden und eine von der Sprache ausgewählt wird (möglicherweise die spezifischste). Dies ist die am wenigsten benommene Art von Polymorphismus, da nichts das Verhalten der beiden Verfahren verbindet, außer der entwickelten Konvention.

Eine dritte Art von Polymorphismus ist der Subtyp Polymorphismus . Hier kann eine Prozedur, die für einen bestimmten Typ definiert ist, auch für eine ganze Familie von "Untertypen" dieses Typs arbeiten. Wenn Sie eine Schnittstelle implementieren oder eine Klasse erweitern, erklären Sie im Allgemeinen Ihre Absicht, einen Untertyp zu erstellen. Wahre Untertypen unterliegen dem Liskovschen Substitutionsprinzip, die besagt, dass wenn Sie etwas über alle Objekte in einem Supertyp beweisen können, Sie es über alle Instanzen in einem Subtyp beweisen können. Das Leben wird jedoch gefährlich, da Menschen in Sprachen wie C ++ und Java im Allgemeinen nicht erzwungene und oft undokumentierte Annahmen über Klassen haben, die in Bezug auf ihre Unterklassen wahr sein können oder nicht. Das heißt, Code wird so geschrieben, als wäre mehr nachweisbar als er tatsächlich ist, was eine ganze Reihe von Problemen mit sich bringt, wenn Sie unachtsam einen Untertyp eingeben.

Die Vererbung ist eigentlich unabhängig vom Polymorphismus. Wenn ein Ding "T" einen Verweis auf sich selbst hat, geschieht die Vererbung, wenn Sie ein neues Ding "S" aus "T" erstellen und den Verweis von "T" auf sich selbst durch einen Verweis auf "S" ersetzen. Diese Definition ist absichtlich vage, da die Vererbung in vielen Situationen vorkommen kann. Am häufigsten wird jedoch eine Unterklasse für ein Objekt festgelegt, bei der der thisvon virtuellen Funktionen aufgerufene Zeiger durch den thisZeiger auf den Untertyp ersetzt wird.

Vererbung ist gefährlich, wie alle sehr mächtigen Dinge, bei denen Vererbung die Macht hat, Chaos zu verursachen. Angenommen, Sie überschreiben eine Methode, wenn Sie von einer Klasse erben: Alles ist in Ordnung und gut, bis eine andere Methode dieser Klasse davon ausgeht, dass sich die von Ihnen geerbte Methode auf eine bestimmte Weise verhält . Sie können sich teilweise davor schützen, indem Sie alle von einer anderen Ihrer Methoden aufgerufenen Methoden als privat oder nicht virtuell (final) deklarieren, es sei denn, sie sind so konzipiert , dass sie überschrieben werden. Auch das ist nicht immer gut genug. Manchmal sieht man vielleicht so etwas (in Pseudo-Java, hoffentlich lesbar für C ++ und C # -Benutzer)

interface UsefulThingsInterface {
    void doThings();
    void doMoreThings();
}

...

class WayOfDoingUsefulThings implements UsefulThingsInterface{
     private foo stuff;
     public final int getStuff();
     void doThings(){
       //modifies stuff, such that ...
       ...
     }
     ...
     void doMoreThings(){
       //ignores stuff
       ...
     }
 }

Sie finden das schön und haben Ihre eigene Art, "Dinge" zu tun, aber Sie nutzen die Vererbung, um die Fähigkeit zu erlangen, "moreThings" zu tun.

class MyUsefulThings extends WayOfDoingUsefulThings{
     void doThings {
        //my way
     }
}

Und alles ist gut und schön. WayOfDoingUsefulThingswurde so konzipiert, dass das Ersetzen einer Methode die Semantik einer anderen Methode nicht verändert ... außer warten, nein, war es nicht. Es sieht einfach so aus, als wäre es so, aber der doThingsveränderbare Zustand hat sich geändert, was wichtig war. Also, obwohl es keine überschreibbaren Funktionen aufgerufen hat,

 void dealWithStuff(WayOfDoingUsefulThings bar){
     bar.doThings()
     use(bar.getStuff());
 }

Jetzt macht man etwas anderes als erwartet, wenn man es einem übergibt MyUsefulThings. Was noch schlimmer ist, vielleicht wissen Sie gar nicht, dass WayOfDoingUsefulThingsdas diese Versprechungen gemacht hat. dealWithStuffKommt möglicherweise aus derselben Bibliothek wie WayOfDoingUsefulThingsund getStuff()wird nicht einmal von der Bibliothek exportiert (denken Sie an Freundesklassen in C ++). Noch schlimmer ist, haben Sie die statischen Kontrollen der Sprache , ohne es zu merken besiegt: dealWithStuffnahm ein , WayOfDoingUsefulThingsnur um sicher zu stellen , dass es eine hätte getStuff()Funktion , die eine bestimmte Art und Weise verhalten hat .

Komposition verwenden

class MyUsefulThings implements UsefulThingsInterface{
     private way = new WayOfDoingUsefulThings()
     void doThings() {
        //my way
     }
     void doMoreThings() {
        this.way.doMoreThings();
     }
}

bringt statische Sicherheit zurück. Im Allgemeinen ist Komposition einfacher zu verwenden und sicherer als Vererbung bei der Implementierung von Subtyping. Sie können damit auch final-Methoden überschreiben, was bedeutet, dass Sie die meiste Zeit alles final / non-virtual deklarieren können, mit Ausnahme von Schnittstellen.

In einer besseren Welt würden Sprachen automatisch das Boilerplate mit einem delegationSchlüsselwort einfügen . Die meisten tun das nicht. Ein Nachteil sind größere Klassen. Sie können jedoch Ihre IDE veranlassen, die delegierende Instanz für Sie zu schreiben.

Jetzt geht es im Leben nicht nur um Polymorphismus. Sie müssen nicht immer einen Untertyp eingeben. Das Ziel des Polymorphismus ist im Allgemeinen die Wiederverwendung von Code, aber es ist nicht der einzige Weg, dieses Ziel zu erreichen. Häufig ist es sinnvoll, die Komposition ohne Subtyp-Polymorphismus zur Verwaltung der Funktionalität zu verwenden.

Auch die Verhaltensvererbung hat ihre Verwendung. Es ist eine der mächtigsten Ideen in der Informatik. Es ist nur so, dass in den meisten Fällen gute OOP-Anwendungen nur mithilfe von Schnittstellenvererbung und Kompositionen geschrieben werden können. Die zwei Prinzipien

  1. Vererbung verbieten oder Design dafür
  2. Ich bevorzuge die Komposition

sind aus den oben genannten Gründen ein guter Leitfaden und verursachen keine erheblichen Kosten.


4
Gute Antwort. Ich würde zusammenfassen, dass der Versuch, die Wiederverwendung von Code durch Vererbung zu erreichen, eindeutig ein falscher Weg ist. Vererbung ist eine sehr starke Einschränkung (wenn man "Macht" hinzufügt, ist das eine falsche Analogie!) Und erzeugt eine starke Abhängigkeit zwischen geerbten Klassen. Zu viele Abhängigkeiten = schlechter Code :) Vererbung (auch bekannt als "verhält sich wie") scheint normalerweise für einheitliche Schnittstellen (= Komplexität von geerbten Klassen verbergen), für alles andere zweimal
überlegen

2
Das ist eine gute Antwort. +1. Zumindest in meinen Augen scheint es so, als ob die zusätzliche Komplikation erhebliche Kosten verursachen könnte, und dies hat mich persönlich ein wenig gezögert, die Bevorzugung von Komposition zu einer großen Sache zu machen. Vor allem, wenn Sie versuchen, viel Unit-Testing-freundlichen Code über Interfaces, Composition und DI zu erstellen (ich weiß, dass hierdurch andere Dinge hinzugefügt werden), scheint es sehr einfach zu sein, jemanden dazu zu bringen, mehrere verschiedene Dateien zu durchsuchen, um sehr nachzuschlagen einige Details. Warum wird dies aber nicht öfter erwähnt, auch wenn es sich nur um das Prinzip der Komposition über Vererbung handelt?
Panzercrisis

27

Der Grund, warum die Leute dies sagen, ist, dass OOP-Programmierer, die ihre Vorlesungen über Polymorphismus durch Vererbung noch nicht abgeschlossen haben, dazu neigen, große Klassen mit vielen polymorphen Methoden zu schreiben.

Ein typisches Beispiel stammt aus der Welt der Spieleentwicklung. Angenommen, Sie haben eine Basisklasse für alle Ihre Spieleinheiten - das Raumschiff des Spielers, Monster, Kugeln usw .; Jeder Entitätstyp hat eine eigene Unterklasse. Der Vererbungs Ansatz würde einige polymorphen Methoden verwenden, zum Beispiel update_controls(), update_physics(), draw()usw., und setzt sie für jede Unterklasse. Dies bedeutet jedoch, dass Sie nicht verwandte Funktionen koppeln: Es ist unerheblich, wie ein Objekt aussieht, um es zu bewegen, und Sie müssen nichts über seine KI wissen, um es zu zeichnen. Der Kompositionsansatz definiert stattdessen mehrere Basisklassen (oder Interfaces), z. B. EntityBrain(Unterklassen implementieren KI- oder Spielereingaben), EntityPhysics(Unterklassen implementieren Bewegungsphysik) und EntityPainter(Unterklassen kümmern sich um das Zeichnen) sowie eine nicht polymorphe KlasseEntitydas hält eine Instanz von jedem. Auf diese Weise können Sie jedes Erscheinungsbild mit jedem Physikmodell und jeder KI kombinieren, und da Sie sie getrennt halten, wird auch Ihr Code viel sauberer. Auch Probleme wie "Ich möchte ein Monster, das wie das Ballon-Monster in Level 1 aussieht, sich aber wie der verrückte Clown in Level 15 verhält", verschwinden: Nehmen Sie einfach die passenden Komponenten und kleben Sie sie zusammen.

Beachten Sie, dass der Kompositionsansatz weiterhin die Vererbung in jeder Komponente verwendet. obwohl Sie hier im Idealfall nur Schnittstellen und ihre Implementierungen verwenden würden.

"Trennung von Anliegen" ist hier der Schlüsselbegriff: Die Darstellung der Physik, die Implementierung einer KI und das Zeichnen einer Entität sind drei Anliegen, deren Zusammenfassung zu einer Entität ein viertes ist. Beim kompositorischen Ansatz wird jedes Unternehmen als eine Klasse modelliert.


1
Das ist alles gut und wird in dem Artikel befürwortet, der in der ursprünglichen Frage verlinkt ist. Ich würde es begrüßen (wann immer Sie Zeit haben), wenn Sie ein einfaches Beispiel für all diese Entitäten liefern und gleichzeitig den Polymorphismus (in C ++) beibehalten könnten. Oder zeige mir eine Ressource. Vielen Dank.
MustafaM

Ich weiß, dass Ihre Antwort sehr alt ist, aber ich habe genau dasselbe getan, wie Sie es in meinem Spiel erwähnt haben, und mich jetzt für Komposition über Vererbung entschieden.
Sneh

"Ich möchte ein Monster, das aussieht wie ..." Wie macht das Sinn? Eine Schnittstelle gibt keine Implementierung, Sie müssten das Aussehen und Verhalten Code auf die eine oder andere Weise kopieren
NikkyD

1
@NikkyD Sie nehmen MonsterVisuals balloonVisualsund MonsterBehaviour crazyClownBehaviourund instanziieren a Monster(balloonVisuals, crazyClownBehaviour), zusammen mit den Instanzen Monster(balloonVisuals, balloonBehaviour)und Monster(crazyClownVisuals, crazyClownBehaviour), die auf Stufe 1 und Stufe 15 instanziiert wurden
Caleth

13

Das Beispiel, das Sie angegeben haben, ist eines, bei dem Vererbung die natürliche Wahl ist. Ich glaube nicht, dass irgendjemand behaupten würde, dass Komposition immer eine bessere Wahl ist als Vererbung - es ist nur eine Richtlinie, die bedeutet, dass es oft besser ist, mehrere relativ einfache Objekte zusammenzusetzen, als viele hochspezialisierte Objekte zu erstellen.

Die Delegierung ist ein Beispiel für die Verwendung der Komposition anstelle der Vererbung. Mit der Delegierung können Sie das Verhalten einer Klasse ohne Unterklassen ändern. Stellen Sie sich die Klasse NetStream vor, die eine Netzwerkverbindung bereitstellt. Es kann natürlich sein, NetStream als Unterklasse einzustufen, um ein gemeinsames Netzwerkprotokoll zu implementieren, sodass Sie möglicherweise FTPStream und HTTPStream entwickeln. Anstatt jedoch eine sehr spezifische HTTPStream-Unterklasse für einen einzigen Zweck zu erstellen, z. B. UpdateMyWebServiceHTTPStream, ist es oft besser, eine einfache alte Instanz von HTTPStream zusammen mit einem Delegaten zu verwenden, der weiß, wie er mit den von diesem Objekt empfangenen Daten umzugehen hat. Ein Grund dafür ist, dass die Anzahl der Klassen, die beibehalten werden müssen, die Sie jedoch nie wieder verwenden können, nicht zunimmt.


11

Sie werden diesen Zyklus im Softwareentwicklungsdiskurs viel sehen:

  1. Einige Funktionen oder Muster (nennen wir sie "Muster X") haben sich für einen bestimmten Zweck als nützlich erwiesen. In Blog-Posts werden die Tugenden von Pattern X gepriesen.

  2. Der Hype führt einige Leute zu denken , sollten Sie Muster X verwenden , wann immer möglich .

  3. Andere Leute ärgern sich darüber, dass Pattern X in Kontexten verwendet wird, in denen es nicht angemessen ist, und sie schreiben Blog-Posts, in denen sie erklären, dass Sie Pattern X nicht immer verwenden sollten und dass es in bestimmten Kontexten schädlich ist.

  4. Das Spiel lässt manche Leute glauben, dass Muster X immer schädlich ist und niemals verwendet werden sollte.

Sie werden feststellen, dass dieser Hype / Backlash-Zyklus bei fast allen Funktionen auftritt, von GOTOMustern über SQL bis hin zu NoSQL und Vererbung. Das Gegenmittel ist, immer den Kontext zu berücksichtigen .

Nachdem CircleAbstieg aus Shapeist genau wie die Vererbung soll in OO - Sprachen verwendet werden , unterstützen Vererbung.

Die Faustregel "Komposition vor Vererbung" ist ohne Kontext wirklich irreführend. Sie sollten die Vererbung vorziehen, wenn die Vererbung angemessener ist, aber die Komposition vorziehen, wenn die Komposition angemessener ist. Der Satz richtet sich an Personen im Stadium 2 des Hype-Zyklus, die der Meinung sind, dass Vererbung überall eingesetzt werden sollte. Aber der Kreislauf hat sich weiterentwickelt, und heute scheinen einige Leute zu glauben, die Vererbung sei an sich schon schlecht.

Stellen Sie es sich vor wie einen Hammer gegen einen Schraubenzieher. Sollten Sie einen Schraubenzieher einem Hammer vorziehen? Die Frage ergibt keinen Sinn. Sie sollten das für den Job geeignete Tool verwenden, und alles hängt davon ab, welche Aufgabe Sie erledigen müssen.

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.