Design: Inheritance vs. Composition ???



  • Ich schau grad ueber aelteren Code von mir ...

    Was mir dabei auffaellt:

    Ich benutze haeufig offentliche Vererbung um vorhandene Klassen zu erweitern.
    Beispiel, nen Smartpointer auf ne Typisierte COM Schnittstelle.
    90% der Funktionalitaet bietet mir schon das ATL Template CComQIPtr ...
    Ich will nur ein paar wenige Funktionen etwas mehr an meine Beduerfniesse anpassen, also Konvertierungen vornehmen etc.

    class MyInterFaceSPtr : public CComQIPtr<IMyInterface>
    {
        // Constructoren durcheichen
        // und Destructor definieren, auch wenn er nix macht
    
        // meine Funktion, die mir komfortabel auf ne Com-Funktion zugreift
        bool GetName(std::basic_string<TCHAR> a_strValue);
    };
    

    Sowas nutze ich recht haeufig.
    In diversen Buechern, z.B. Exeptional C++, steht:
    Erben sie niemals oeffentlich, um Code wiederzuverwenden! Erben sie nur öffentlich, um selbst weiderverwendet zu werden.

    Genau dagegen verstosse ich ja.
    1. Meine Klasse ist zwar ein CComQIPtr, aber ein erweiterter, und mehr spezialisierter
    2. Ich erbe aber nicht, um Polymorphie zu benutzen !!!

    Bin ich nun Poese ? 😃
    Bedeutet sauberes Design wirklich, dass ich fuer eine winzige erweiterung Tonnen an schon vorhandener funktionalitaet an ne Klassmember weiterleiten muss ?
    Die antwort steht in keinem Buch ... 😞 Oder ich habs nur ned gefunden ...

    Ciao ...



  • Hallo,
    wenn die Basisklasse keine protected-Schnittstelle hat und keine virtuellen Methoden (insbesondere keinen virtuellen Dtor), dann ist die öffentliche Vererbung aus Design-Sicht schwachfug. Du (als Client der Klasse) gewinnst nichts durch die öffentliche Vererbung, erkaufst dir das aber mit maximalen Abhängigkeiten. Vielleicht sparst du dir als Implementierer der Klasse ein wenig Tipparbeit. Diese Perspektive ist aus Design-Sicht aber kein guter Ratgeber.
    Private Vererbung kann unter seltenen Umständen noch sinn machen.
    Composition ist gut. Und wenn's geht, warum nicht schlicht und einfach freie Funktionen verwenden? (nebenbei: da du ja EC++ erwähnt hast, kennst du ja sicher auch das Interface-Prinzip)



  • Danke fuer die Antwort und die Kritik ! 😃

    du dir als Implementierer der Klasse ein wenig Tipparbeit

    Genau darum geht es ja 🙂

    Das Problem: Ich habe Tonnen von Schnittstellen die ich kapsle ... im moment hab ich halt dann tonnen von abgeleiteter/spezialisierter klassen, kein Problem.

    Und wenn's geht, warum nicht schlicht und einfach freie Funktionen verwenden?

    geht nicht besonders gut, weil die Funktionen die ich Komfotabler machen will, alle unterschiedliche namen haben, und damit auch nicht generell zu nem CComQIPtr passen, sondern zu genau einer spezialisation. und damit haett ich tonnen von freien funktionen ...
    Die Funktionen haben keinerlei programmteschnisch logischen Inhalt, wenn die es haetten, wuerden sie in das Object selber, also in die Schnittstelle wandern.
    EIgentlich sind es nur Konvertierungs-funktionen, weil die COM-Funktionen mir ja BSTRs liefern, ich aber meist nur mit TCHARS was anfangen kann.

    Ich muesst also jedesmal wenn ich so ne Funktion nutze, die ATL-Conversion Makros nutzen, oder selbst konvertieren ... das sieht <zensiert> aus.
    (ATL-Conversion Makros sollt man eh nur in kleinen funktionen mit smalen scope nutzen )

    Und Komposition ist zwar saueberer, aber ... so nen CComQIPtr hat ca 20 funktionen / operatoren, die ich auch brauch ! die muesst ich durchreichen, und das fuer ca. 2 oder FUnktionen am Pointer selbst, die ich komfortabler machen will ...

    Was helfen wuerde ... wern Generator, der die COM schnittstelle abfragt, und die wrapperklasse daraus erzeugt, inklusive komfortablerer funktionen fuer die BSTR geschichte. Der koennte kompositon verwenden, weil tipparbeit waer ja da egal.
    Bei der MFC ist sowas bei, die kann solche wrapperklassen erzeugen, geht aber eben nur wenn MFC verwendest 😞 Ich braucht so nen Addin fuer die ATL

    Ciao ...



  • Die Funktionen haben keinerlei programmteschnisch logischen Inhalt, wenn die es haetten, wuerden sie in das Object selber, also in die Schnittstelle wandern.

    Das ist der Denkfehler. Eine freie Funktion, die zusammen mit einer Klasse kommt (z.B. im selben Namensraum liegt) und die ein Objekt dieser Klasse erwartet, gehört zum Interface der Klasse. Sie ist Teil der Schnittstelle.
    So ist C++. Die Schnittstelle eines Objekts ist nicht ausschließlich in der passenden Klassendefinition zu suchen.

    Puristen haben damit häufig ein Problem, da sie mal foo.func() und mal func(foo) schreiben müssen. Das ist aber lediglich ein syntaktischer Unterschied.

    Ich sehe die Möglichkeiten wie folgt:
    Gegeben eine nicht-polymorphe, nicht als Basisklasse gedachte Stringklasse namens String.
    Nach Monaten der fröhlichen Benutzung, stellt man fest, dass man gerne eine toUpper-Funktion in der Klasse hätte.
    Möglichkeiten:
    1. Änderung der Klasse Originalklasse:
    --------------------------------------
    Vorteile: Einfach (nur eine Methode muss implementiert werden), sauber, transparent für bestehnden Code. Keine Casts notwendig.
    Nachteile: Man braucht den Source der Klasse. Man muss bei Updates immer wieder seine Ergänzungen einbauen -> Starke Abhängigkeit.

    2. Öffentlich Ableiten:
    Vorteil: Relativ einfach (nur Konstruktoren müssen durchgereicht werden). Transparent für bestehenden Code.
    Nachteil: Überall wo man nur Basisklassenreferenzen hat, muss man, ganz im Gegensatz zum Sinn der öffentlichen Vererbung, zum richtigen Typ downcasten, wenn man dessen Erweiterungen benutzen will -> Man wird zu casts verleitet
    -> Das kann schnell hässlich werden.

    Ein großer Kommentar am Anfang der Klasse muss darauf hinweisen, dass Objekte *niemals* über eine Basisklassenreferenz gelöscht werden dürfen. Das ist eine Konvention die der Compiler leider nicht prüfen kann. Funktioniert nur solange
    gut, solange keine Java-Programmierer ins Team kommen.

    Starke Abhängigkeit von der Basisklasse.

    3. Geschützt/Private Ableiten:
    Vorteil: Relativ einfach (nur Konstruktoren müssen durchgereicht werden). Drückt die Situation besser aus als öffentliche Vererbung.
    Nachteil: Nicht transparent für bestehenden Code. Das ist aber gleichzeitig auch ein Vorteil, da man gar nicht erst auf die Idee kommt ein NewString-Objekt über Basisklassenreferenzen zu verwenden -> man wird nicht zum Casten verleitet.

    Starke Abhängigkeit von der Basisklasse.

    4. Composition:
    Vorteil: Ähnlich wie drittens nur mit weniger Abhängigkeiten von der Originalklasse.
    Nachteil: Tippaufwendig (alle Konstruktor + Methoden müssen durchgereicht werden). Kann man aber natürlich umgehen, in dem man eine get-Methode einbaut, die den zugrundeliegenden Typ liefert.
    Nicht transparent -> kann aber wiederum auch von Vorteil sein, da genauer ausgerdückt wird, worum es sich hier wirklich handelt.

    5. freie Funktion:
    Vorteil: Einfach, sauber, transparent für bestehnden Code, geringe Abhängigkeit von der Orginalklasse.
    Nachteil: Änderung der Aufrufsyntax.

    Mir gefällt 5. am Besten.



  • 2, 3 und 4 klingen auf den ersten Blick designtechnisch sauber, aber praktisch gesehen ist es IMHO Unfug, zwei Stringtypen danach zu unterscheiden, ob sie toupper anbieten oder nicht. Ich bin ganz klar für 5, wobei ich dazusagen muss, dass ich es besser fände, wenn Methoden generell in der Form method(obj1, obj2, ...) aufgerufen würden, dann würde die syntaktische Unterscheidung und somit der Nachteil von 5 wegfallen. Leider ist das natürlich in C++ aus verschiedenen Gründen nicht machbar und ausserdem nur MHO.



  • Nachteil bei 5 ist, daß man damit andere Nutzer zum Wahnsinn treiben kann, weil die Wahl des Syntax (intern oder extern) dann von solchen "leichten" Kriterien wie "hängt nicht von der Basisfunktionalität der Klasse ab, also extern" bestimmt wird. Und dieses Unterscheidungskriterium wird wiederum vom Entwickler der Klasse nach "eigenem Wissen und Gewissen" festgelegt.

    Man kann die Variante 5 nämlich auch wie folgt pervertieren:

    class Vector2
    {
    public:
       double getX() const {return m_X;}
       double getY() const {return m_Y;}
       void setX(int x) {m_X = x;}
       void setY(int y) {m_Y = x;}
       Vector2() : m_X(0), m_Y(0) {}
    
    private:
       double m_X;
       double m_Y;
    };
    
    double length(const Vector2& vec)
    {
       return sqrt(vec.getX() * vec.getX() + vec.getY() * vec.getY());
    }
    

    Das Beispiel ist nur rudimentär, zeigt aber eine Problemrichtung auf:

    Die Forderung kann in dem Sinne mißverstanden werden, alle Attribute komplett über set/get erreichbar zu machen und alle Member extern zu legen. Da hat man dann aber eigentlich nur kompliziert C und struct simuliert.

    Und die Grenze zwischen so einem Extrembeispiel und einer vernünftigen Anzahl externer Funktion zu ziehen überlässt man alleine der Vernunft des Klassenentwicklers.

    Dagegen ist die Forderung "alles was das Objekt der Klasse manipuliert, ist Teil der Klasse" zumindest in dieser Hinsicht relativ eindeutig.

    Um die Idee von Herb Sutter zu realisieren fand ich die Idee gut, wie sie mal bei der Diskussion in "Rund um" geäußert wurde (ich glaube von Kartoffelsack), lieber eine Kernel-Klasse mit Minimalfunktionalität schreiben und außen herum die "Methoden 2. Klasse" in einen Wrapper drumherum. Verhindert auch zu starke gegenseitige Abhängigkeiten, schafft innerhalb der äußeren Klasse eine Firewall, aber die Zugehörigkeit bleibt eindeutig.



  • HumeSikkins schrieb:

    Die Funktionen haben keinerlei programmteschnisch logischen Inhalt, wenn die es haetten, wuerden sie in das Object selber, also in die Schnittstelle wandern.

    Das ist der Denkfehler. Eine freie Funktion, die zusammen mit einer Klasse kommt (z.B. im selben Namensraum liegt) und die ein Objekt dieser Klasse erwartet, gehört zum Interface der Klasse.

    Neee, Du hasst mich da falsch verstanden ! 😃

    Ich meinte, die Funktion, die ich selber dazuimplementieren will, hat keinen programmtechnisch-logischen Hintergrund, sondern einen systemtechnischen.
    einfach um den String zu konvertieren, nicht um ihn Logisch(also inhaltlich) zu veraendern.
    Wuerd ich ne Funktion brauchen, die mir aus 3 Teilstrings der Klasse nen 4. Zusammenbaut, wuerd ich das in die eigentliche Com-Klasse mit aufnehmen, weil ich erwarten wuerde, das andere Programmierer den 4. String vielleicht aauch brauchen, und die Com klasse nicht in c++ verwenden, dann muessten sie die selbe funktionalitaet in Java/Basic auch nachprogrammieren. Ok, in diesem Falle bin ich auch der Autor der COM klasse, weshalb mir das auch moeglich macht. Und Interface <-> Com object sind eh ne starke Beziehung. Aber das ist in diesem Fall ne System-Problematic ...
    Und da COM Objecte sprachunabhaengig sein (sollten)... ! Eine zusaetzliche freie funktion wuerde in C++ zur Verfuegung stehen, aber nimmer fuer Bais/Java ...etc
    Fuer Sprachspezifische Probleme (konvertierung) ists aber kein problem ...

    Um noch mal auf das eigentliche Problem zurueckzukommen + freie funktion ...
    Meine Klasse ist eigentlich nen Template

    CComQIPtr<IMyInterface> p_test;
    die klasse ueberlead den -> operator,der ne referenz auf die InterfaceKlasse gibt, so das ich da schreiben kann:
    Result = p_test->Get_myValue(&ValueBuffer);
    so werden die Funktionen vom COM Interface spezifieziert.
    Manchmal doof zu haendeln, geht ned anders ...
    Richtig eklig wirds mit Strings, weil die vom Typ BSTR(OLE Datentyp) sind, ich aber TCHars brauch.
    Fuer BSTRs stellt die ATL ne super klasse zur Verfuegung ... die mir das Benutzen einfach macht (die ueberschreibt sogar den & Operator, so das ich die Klasse wie nen Zeiger selber behandeln kann). Aber leider hat der keinerlei untertuetzung fuer die konvertierung zurueck in nen "normalen" string. Hinverwandlung ist implementiert mittels CTor und = operator !

    Die Konvertierung aus der Klasse rausnehmen ist vielleicht schon mal keine schlechte Idee, weil logisch hat sie in der Klasse auch ned viel zu suchen.

    Ich denk mal der "bessere" weg ist nicht freie funktionen zu benutzen, sondern ne weiter klasse fuer Strings schreiben, oder ne Bestehende erweitern, welche operatoren und CTors fuer BSTRs beinhalten ... damit ich die konvertierungsmakros aus dem quelltext bekomme.
    Die klasse brauch ich dann nur einmal zu schreiben ...

    Ciao ...



  • Marc++us schrieb:

    Das Beispiel ist nur rudimentär, zeigt aber eine Problemrichtung auf:

    Die Forderung kann in dem Sinne mißverstanden werden, alle Attribute komplett über set/get erreichbar zu machen und alle Member extern zu legen. Da hat man dann aber eigentlich nur kompliziert C und struct simuliert.

    Klar, das liegt daran, dass ein Vektor grundsätzlich auch nichts weiter ist als eine dumme Struktur. Objekte sollen Verhalten haben, nicht nur bloße Datenhalter sein. Sind sie es doch, bringt es IMHO nichts, das zu verschleiern.



  • Ich glaube Du hast den von mir angesprochenen Punkt nicht erkannt oder willst ihn nicht sehen, da Du Dich an dem Beispiel festmachst. Der Vektor war einfach ein Beispiel. Ich hatte jetzt keine Lust als Beispiel eine Verwaltungsklasse für Hotelzimmer hier einzutippen.

    Selbst bei diesem Minibeispiel Vektor würde man ja nach OO-Lehre die Länge eher als berechnetes Attribut betrachten, d.h. daher eher als Member auslegen.

    Die Gefahr besteht aber, daß wohlmeinende Verfechter der schlanken Schnittstelle nachher plötzlich wieder jede Klasse in Datencontainer umwandeln.



  • Obwohl ich auch der Meinung bin, dass man vererben auch wegen Tipparbeit machen darf, oder dürfen sollte, da es die Codepflege erleichtert bin ich mir bei Deinem Fall jetzt nicht so wirlich sicher, ob das Sinn macht. Hast Du X Klassen und leitest jede ab weil Du für jede Funktion die einen BSTR liefert ein TChars-Pondon in die Klasse aufnehmen willst? Dann würd ich mir doch lieber eine einzige Konvertierungsfunktion schreiben.



  • Dann würd ich mir doch lieber eine einzige Konvertierungsfunktion schreiben.

    Deswegen bin ich auf die "loesung" mit den Funktionen gekommen ...
    Ich bekomm nen zeiger auf nen BSTR, allociert, fuer den bin ich verantwortlich.
    Ok, ich hab/brauch ne funktion ... die mir das Teil in nen Tchar umwandelt.
    Speicher allocieren, ist auch kein problem ... ich bekomm nen Zeiger auf nen Allocierten Tchar. bin ich auch fuer verantwortlich ...

    Als Stringklasse wollt ich std::basic_string<TCHAR> nehmen ... das doofe, die hat kein Attach() und Detach(), sprich ich kann allokierte Zeiger nicht einfach uebernehmen (was ja auch ned sinn der std strings ist )... also kopier ich nochmal ???
    und muss 2 strings wieder freigeben .. fand ich unschoen.

    Ne alternative waeren die Consversations-Makros ... die gaengen sogar ziemlich einfach zu benutzen ...
    basic_string<TCHAR> myTstring = OLE2T(myBSTR);
    nu brauch ich nur 1 mal freigeben , den BSTR wieder
    Nur was das Makro draus macht ... ist auch ned feierlich ... es wird lokal irgendwie nen buffer angelegt, wo das makro dann nen stirng drin erzeugt und dir nen constanten Zeiger drauf zurueckgibt ...
    M$ warnt auch selber for ausgiebigen gebrauch dieser Dinge, sie tun manchmal ned immer dass ... deshalb sollte man sie auch nur lokal in functionen mit kurzen scope und niemals viele dieser makros im selben scope aufrufen. Stand irgendwann mal in so nem MSDN Workaround.

    Zusaetzliche Motivation war fuer mich auch, den resultwert der COM funktion immer gleich mitabzufangen. Wenn der Reuckgabewert nicht ok ist, garantieren die COM funktionen fuer nix was an der uebergebenen adresse steht. normal sollte zwar NULL drinn stehen ... Deshalb hab ich die Rudimentaere fehlerbahndlung fuer die COmfehler und das Initialiseren der Strings gern in so einer funktion gehabt ...

    Der arbeitsaufwand fuer das Ableiten jeder Schnittselle war meiner meinung nach nicht wesentlich viel arbeit mehr als jedesmal nen Fehler nach jedem funktionsaufruf abzufangen ... Wenn ich die schnittstellen ziemlich extensiv genutzt hab, hat sich das sogar gelohnt 🙂

    Ciao ...


Anmelden zum Antworten