Favour Composition over Inheritance - Aber Wann?
-
volkard schrieb:
Ein Student IST eine Person. Das ist Vererbung.
Das ist ja das was ich meinte. Ich habe schon oft gesehen das in solchen Is-A-Beziehungen dennoch manchmal Komposition angewendet wird. Mir ist auch klar das dieser Fall den der Dozent beschreibt definitiv nach Vererbung schreit, haette halt nur gedacht das man es in diesem Fall auch mal mit Komposition probieren kann.
Aber so weiss ich immer Bescheid das man definitiv Vererbung benutzen sollte wenn es auch definitiv eine Is-A-Beziehung ist.
-
pumuckl schrieb:
Firefighter schrieb:
mich wuerde nur mal Interessieren in welchen Faellen denn eine Komposition der Vererbung vorzuziehen ist.
Siehs andersrum. Der alte Irrglaube "Objektorientiert = Vererbung" verleitet dazu, erstmal Vererbung als gegeben anzunehmen und sich dann zu fragen "wo wäre Komposition besser?" - so wie du es auch formuliert hast.
Tatsächlich sollte man andersrum die Komposition als default nehmen und dann fragen "wo sollte ich stattdessen Vererbung benutzen?".Die Antwort auf DIE Frage ist dann nämlich duetlich einfacher, allerdings von der Sprache abhängig. Einige Beispiele, die ein deutliches Indiz für Vererbung sind (das heißt nicht, dass Vererbung dann ein Muss ist!):
- Polymorphie wird benötigt. (vgl. aber auch Compiletime-Polymorphie durch Templates)
- Die Basisklasse unterliegt nicht deiner Verantwortung und du brauchst z.B. Zugriff auf protected Member (d.h. Vererbung als "hack" für unzureichendes Design)
- Viele Template-Idioms benutzen Vererbung (z.B. CRTP, spezielle Anwendungen des Barton-Nackmann-Trick, Policy based Design,...)
In deinem Fall wäre es wohl ein ziemliches Gewürge, keine Vererbung zu benutzen...
Das find ich Spitze, super Erklaerung und den Blickwinkel hier umzudrehen find ich auch gut, ich glaub das wird mir in Zukunft noch mehr helfen.
Ich will es nochmal betonen, mir ist selbst klar das ich hier Vererbung einsetzen muss, ich hab das Beispiel jedoch mit Absicht gewaehlt um herauszufinden wann diese Aussage FCoI ueberhaupt anwendung findet.
-
Firefighter schrieb:
Aber so weiss ich immer Bescheid das man definitiv Vererbung benutzen sollte wenn es auch definitiv eine Is-A-Beziehung ist.
…und jeder wissen darf, daß eine Is-A-Beziehung herrscht. Also wenn man public vererbt.
So könnte ich mir einen Ringpuffer vorstellen, der eigentlich ein Array IST, aber noch zwei Zeiger dazu hat und push() und pop(). Es wird auch keiner Widersprechen, wenn man die Beschreibung beginnt mit "Ein Ringpuffer ist ein Array, das…".
Und schon haben wir den Salat. Welche Regel greift hier? Doch schlichtes FCoI?
-
volkard schrieb:
Firefighter schrieb:
Aber so weiss ich immer Bescheid das man definitiv Vererbung benutzen sollte wenn es auch definitiv eine Is-A-Beziehung ist.
…und jeder wissen darf, daß eine Is-A-Beziehung herrscht. Also wenn man public vererbt.
So könnte ich mir einen Ringpuffer vorstellen, der eigentlich ein Array IST, aber noch zwei Zeiger dazu hat und push() und pop(). Es wird auch keiner Widersprechen, wenn man die Beschreibung beginnt mit "Ein Ringpuffer ist ein Array, das…".
Und schon haben wir den Salat. Welche Regel greift hier? Doch schlichtes FCoI?Gute Frage, aber sowas wuerde ich schon mit FCoI loesen.
-
Firefighter schrieb:
Gute Frage, aber sowas wuerde ich schon mit FCoI loesen.
Andererseits kannst du dann deinen Ringpuffer nicht dort verwenden, wo du auch ein Array verwenden könntest. Wenn die Implementation aber so schön ist, dass du überall dort, wo du ein Array hast, auch einen Ringpuffer verwenden könntest, dann spricht das deutlich für Vererbung.
-
otze schrieb:
Firefighter schrieb:
Gute Frage, aber sowas wuerde ich schon mit FCoI loesen.
Andererseits kannst du dann deinen Ringpuffer nicht dort verwenden, wo du auch ein Array verwenden könntest. Wenn die Implementation aber so schön ist, dass du überall dort, wo du ein Array hast, auch einen Ringpuffer verwenden könntest, dann spricht das deutlich für Vererbung.
Natuerlich auch wieder korrekt. Ich find es ist sehr schwer diese Aussage zu validieren, mich wundert es halt das es dennoch so oft in vielen Clean-Code Buechern vorkommt und propagiert wird. Mich beschaeftigt die Frage schon lange, hab bisher nur nie ein Beispiel finden koennen.
-
otze schrieb:
Firefighter schrieb:
Gute Frage, aber sowas wuerde ich schon mit FCoI loesen.
Andererseits kannst du dann deinen Ringpuffer nicht dort verwenden, wo du auch ein Array verwenden könntest. Wenn die Implementation aber so schön ist, dass du überall dort, wo du ein Array hast, auch einen Ringpuffer verwenden könntest, dann spricht das deutlich für Vererbung.
Das ist wieder von der Sprache abhängig. In C++ würde ich das über freie Funktionen und Templates machen. Siehe
boost.circular_buffer
der sich wie alle anderen Container verhält.
-
Firefighter schrieb:
mich wundert es halt das es dennoch so oft in vielen Clean-Code Buechern vorkommt
Tja, die Clean-Code-Bewegung halte ich für eine Pest.
Sie ruft geradezu danach, mit religiösen Inhalten gefüllt zu werden.
-
otze schrieb:
Firefighter schrieb:
Gute Frage, aber sowas wuerde ich schon mit FCoI loesen.
Andererseits kannst du dann deinen Ringpuffer nicht dort verwenden, wo du auch ein Array verwenden könntest. Wenn die Implementation aber so schön ist, dass du überall dort, wo du ein Array hast, auch einen Ringpuffer verwenden könntest, dann spricht das deutlich für Vererbung.
Das hängt davon ab, wie das Array definiert ist. Weder C-Arrays noch std::array oder std::vector sind geeignet, davon abzuleiten, geschweige denn dass sie ein (runtime-)polymorphes Interface anbieten. Von diesen Arrays würde ich also keinen Rungpuffer ableiten.
-
volkard schrieb:
Firefighter schrieb:
mich wundert es halt das es dennoch so oft in vielen Clean-Code Buechern vorkommt
Tja, die Clean-Code-Bewegung halte ich für eine Pest.
Sie ruft geradezu danach, mit religiösen Inhalten gefüllt zu werden.Ich wuerde jetzt nicht gleich "Pest" sagen.
Einige Ansatzpunkte sind schon recht Wichtig die sie aufzaehlen, aber genau das FCoI ist eines wo ich mir nicht sicher bin ob das wirklich einen Mehr-Nutzen bringt.
-
Personen, Studenten, Beschaeftigte, Professoren
Klingt klar nach Vererbung.
Allerdings musst du aufpassen:class Hiwi : public Student, public Beschaeftigter { ... };
und Student und Beschaeftigte erben jeweils von Personen -> Diamanten-Problem
Für diesen Fall müsstest du dann evtl. über virtuelle Vererbung nachdenken.
-
Tachyon schrieb:
Das ist wieder von der Sprache abhängig. In C++ würde ich das über freie Funktionen und Templates machen. Siehe
boost.circular_buffer
der sich wie alle anderen Container verhält.Und ob da sgeht, ist wieder Situationsabhängig:
class MyClass{ template<class Array> virtual void foo(Array const&); };
oops.
@pumuckl Das ist klar, dass das mit std::vector nicht (unbedingt) funktioniert. Aber guter Punkt.
@Q Der Fall ist doch DAS Argument für FCoI.
-
otze schrieb:
class MyClass{ template<class Array> virtual void foo(Array const&); };
oops.
Häh
-
virtuelle templates gehen nicht.
-
otze schrieb:
virtuelle templates gehen nicht.
Aber was hat das mit dem zu tun, was ich sagte?
-
Du hast einen Vorschlag ohne Vererbung gebracht mit "In C++ würde ich das über freie Funktionen und Templates machen.". Und ich zeige dir ein C++ Beispiel wo dieser Ansatz zu virtuellen templates führen muss: immer dann, wenn Polymorphie im Spiel ist. Das Problem hast du nicht, wenn du direkt ableitest, sofern es in dem Kontext möglich ist.
Die Frage die FCoI impliziert: nämlich, "Wann braucht man Vererbung?" ist nicht pauschal lösbar. Man kann nicht sagen: "In Sprache X machst du das so". Bevor die Sprache ins Spiel kommt ist das eine Frage des Softwareentwurfs. Danach kommen erst die möglichen Implementationen ins Spiel.
-
otze schrieb:
Du hast einen Vorschlag ohne Vererbung gebracht mit "In C++ würde ich das über freie Funktionen und Templates machen.". Und ich zeige dir ein C++ Beispiel wo dieser Ansatz zu virtuellen templates führen muss: immer dann, wenn Polymorphie im Spiel ist. Das Problem hast du nicht, wenn du direkt ableitest, sofern es in dem Kontext möglich ist.
Die Frage die FCoI impliziert: nämlich, "Wann braucht man Vererbung?" ist nicht pauschal lösbar. Man kann nicht sagen: "In Sprache X machst du das so". Bevor die Sprache ins Spiel kommt ist das eine Frage des Softwareentwurfs. Danach kommen erst die möglichen Implementationen ins Spiel.
a) Können freie Funktionen nicht virtuell sein, weshalb Dein Beispiel etwas am von mir Geschriebenen vorbei geht.
b) Wollte ich auf die Standardalgorithmen und die Container hinweisen, wo Du Compilezeitpolymorphie hast. Aus sicht der Standardalgorithmen ist alles, was sich wie ein Randomaccess-Iterator verhält auch ein... nun ja Randomaccess-Iterator. Auch wenn Du nichtbla.puh()
schreibst.
-
otze schrieb:
Die Frage die FCoI impliziert: nämlich, "Wann braucht man Vererbung?" ist nicht pauschal lösbar. Man kann nicht sagen: "In Sprache X machst du das so". Bevor die Sprache ins Spiel kommt ist das eine Frage des Softwareentwurfs. Danach kommen erst die möglichen Implementationen ins Spiel.
Danke, das lief mir grad naemlich wieder in die Falsche Richtung.
-
Tachyon schrieb:
a) Können freie Funktionen nicht virtuell sein, weshalb Dein Beispiel etwas am von mir Geschriebenen vorbei geht.
Dann geht deine erste Antwort aber an meinem Post vorbei:
Wenn die Implementation aber so schön ist, dass du überall dort, wo du ein Array hast, auch einen Ringpuffer verwenden könntest, dann spricht das deutlich für Vererbung.
//edit (heute editiere ich zu viel...)
Mir geht es eigentlich nicht um den Gegensatz zwischen dynamischer<->statischer Polymorphie. Das ist für mich ein reines Implementationsdetail. Wenn der Softwareentwurf es zulässt, eine polymorphie zur Compilezeit aufzulösen, dann implementiert man das so. Wenn es nicht geht, dann geht es nicht. Die Sprachgegebenheiten sind da zweitrangig vor dem Entwurf.
-
otze schrieb:
[...]ohne Vererbung gebracht [...] immer dann, wenn Polymorphie im Spiel ist.
Merkst was?
Was Tachyon meinte ist eben, dass er eben NICHT den Ansatz über eine vererbte oder vererbbare Klasse gehen würde, sondern über templates. Ohne Vererbung und ohne dass Vererbung für irgendwann später vorgesehen wäre, und dementsprechend ohne dass es irgendwann zu virtuellen Methoden kommen müsste.
C++ als Multi-Paradigmensprache hat genau da einige Fallstricke, im Vergleich zu "rein objektorientierten" Sprachen wie Java oder C#: in letzteren ist so ziemlich alles vererbbar und geerbt (mindestens von Object). In C++ heißt eine Klasse noch lange nicht, dass es was mit Vererbung auch nur zu tun hat. Es gibt genügend Klassen, die by design implizit nicht für Vererbung geeignet sind. Dann gibt es Klassen, die (private) Vererbung als eine Art der Kompositoin mit engerer Bindung benutzen, und Klassen, die die Mechanismen von Vererbung in Zusammenhang mit Templates benutzen, und es gibt Klassen, die tatsächlich als Teil einer echten objektorientierten Vererbungshierarchie anzusehen sind, wie man sie im Lehrbuch findet. Diese Vielfältigkeit macht es vor allem Ein- und Umsteigern in C++ nicht unbedingt einfach. Sie ist auch der Grund, warum das FCoI der CleanCoder die afaik häufiger aus dem "saubereren" Umfeld der Java/C#-Ecke kommen, im C++-Kontext nicht so blindlings angewandt werden kann.