Dependency Injection (Unit-Testing) auch in C++ sinnvoll?





  • Weil C++ keine Reflection-API hat



  • Danke für eure nützlichen Beitrage!

    Ich habe zu euren Antworten ein bisschen recherchiert und es gibt wohl mehrere Möglichkeiten:

    1. Templates
    2. Übergabe der Abhängigkeiten als Referenz / Pointer
    2. a) Constructor
    2. b) Setter
    3. Verwendung eines IoC Containers z.B. http://stackoverflow.com/questions/352885/dependency-injection-in-c

    Richtig schlüssig darüber, welcher Methode der Vorzug zu geben ist, bin ich mir noch nicht.



  • volkard schrieb:

    Flasher.d84 schrieb:

    Richtig etabliert scheint Dependency Injection unter C++ nicht zu sein.

    Na, doch, eher schon. Nur machen wir das, ohne dafür ein Wort zu haben.
    http://www.c-plusplus.net/forum/289092

    So ist es. Ich würde sogar behaupten, dass "Dependency Injection" das ist, was jeder erfahrene Programmierer sowieso intuitiv tun würde. Das wird schon viel länger so gemacht als es das Buzzword gibt 😉



  • Sorry dot, aber das nehme ich dir bzw. anderen C++ Programmierern nicht ab, daß durchgängig alle Abhängigkeiten per DI programmiert sind.
    Dann dürfte es keine direkten Member (von anderen Klassen) in dem C++ Code geben, sondern alles müßte als Zeiger/Referenz übergeben werden.
    Es geht bei DI nicht nur um Abhängigkeit zu externen Resourcen (z.B. Datenbank, FileSystem etc.), sondern generell um einen service-orientierten Ansatz sowie um die Testbarkeit einer einzelnen Klasse.
    Außerdem müßte es dann ja in jedem C++ Programm einen Controller als Hauptprogramm geben, welcher alle Instanziierungen am Anfang ausführt bzw. als Factory dient.

    Um es in Code auszudrücken - sowohl

    class A
    {
    private:
      B b;
    
    public:
      A()
      {
        b.DoSomething();
      }
    }
    

    als auch

    void A::Execute()
    {
      B b;
      // bzw.
      B* b = new B();
    }
    

    widersprechen damit dem Grundgedanken von IoC bzw. DI, da B nun nicht mehr austauschbar ist (sei es durch eine Mock-Implementation oder durch eine andere konfigurierbare Klasse).

    Richtig wäre also nur etwas wie (hier DI per Konstruktor):

    class A
    {
    private:
      B& b;
    
    public:
      A(B& b)
        b(b)
      {
        b.DoSomething();
      }
    
      void DoIt()
      {
        b.DoSomething();
      }
    }
    

    Und B müßte jetzt eine abstrakte Klasse mit pure virtuellen Funktionen (Methoden) sein (damit die Implementierung eben austauschbar ist - in C# oder Java werden dafür eben Interfaces benutzt).
    Und dies dann durchgängig über das gesamte Programm hinweg, so daß jede Klasse einzeln getestet werden kann.



  • Th69 schrieb:

    Sorry dot, aber das nehme ich dir bzw. anderen C++ Programmierern nicht ab, daß durchgängig alle Abhängigkeiten per DI programmiert sind.
    Dann dürfte es keine direkten Member (von anderen Klassen) in dem C++ Code geben, sondern alles müßte als Zeiger/Referenz übergeben werden.
    Es geht bei DI nicht nur um Abhängigkeit zu externen Resourcen (z.B. Datenbank, FileSystem etc.), sondern generell um einen service-orientierten Ansatz sowie um die Testbarkeit einer einzelnen Klasse.
    Außerdem müßte es dann ja in jedem C++ Programm einen Controller als Hauptprogramm geben, welcher alle Instanziierungen am Anfang ausführt bzw. als Factory dient.

    Um es in Code auszudrücken - sowohl

    class A
    {
    private:
      B b;
    
    public:
      A()
      {
        b.DoSomething();
      }
    }
    

    als auch

    void A::Execute()
    {
      B b;
      // bzw.
      B* b = new B();
    }
    

    widersprechen damit dem Grundgedanken von IoC bzw. DI, da B nun nicht mehr austauschbar ist (sei es durch eine Mock-Implementation oder durch eine andere konfigurierbare Klasse).

    Richtig wäre also nur etwas wie (hier DI per Konstruktor):

    class A
    {
    private:
      B& b;
    
    public:
      A(B& b)
        b(b)
      {
        b.DoSomething();
      }
    
      void DoIt()
      {
        b.DoSomething();
      }
    }
    

    Und B müßte jetzt eine abstrakte Klasse mit pure virtuellen Funktionen (Methoden) sein (damit die Implementierung eben austauschbar ist - in C# oder Java werden dafür eben Interfaces benutzt).
    Und dies dann durchgängig über das gesamte Programm hinweg, so daß jede Klasse einzeln getestet werden kann.

    Das sehe ich anders. :xmas2:
    Man ist nicht gesetzlich gezwungen, in C++ so zu programmieren, als sei es in Java/C#. Und im Allgemeinen sollte man das auch nicht tun.



  • volkard schrieb:

    Th69 schrieb:

    ... Abhängigkeiten per DI ...

    Das sehe ich anders. :xmas2:
    Man ist nicht gesetzlich gezwungen, in C++ so zu programmieren, als sei es in Java/C#. Und im Allgemeinen sollte man das auch nicht tun.

    Dann erklär doch mal die du DI in C++ umsetzt.



  • Th69 schrieb:

    class A
    {
    private:
      B& b;
    
    public:
      A(B& b)
        b(b)
      {
        b.DoSomething();
      }
    
      void DoIt()
      {
        b.DoSomething();
      }
    }
    

    Na so 😕



  • volkard meinte, dass er es nicht so macht. 🙄



  • volkard kann templates. und er wählt unter vielen wegen einen, der zum konkreten problem passt. zu den unit tests sagt er nichts, weil er seine testgetriebene zeit hinter sich hat.



  • Ihr redet ein wenig an einander vorbei.
    dot zeigte, wie man das machen kann und das _oft_ auch geschieht. Th69 meinte, dass das nicht so durchgängig angewandt wird von C++ Programmierern, was wohl auch stimmt.
    dot wollte wohl kaum damit sagen, dass das immer alle tun und alle Abhängigkeiten so modeliert sind, sondern nur, dass es dort, wo es passend ist (sicher nicht überall) so gemacht wird.



  • So aneinander vorbei reden wir nicht. DI, so wie es in Java oder C# häufig angewendet wird, also mit Interfaces, IoC Container und schön fürs Testen konfigurierbar usw., macht eigentlich in C++ fast keiner. Darum nennen wir es in C++ auch nicht DI. Nur wollen einige hier halt so tun, als ob C++ schon lange DI vor Java machte und schaffen damit eigentlich nur verwirrung, dot wird gleich nochmal erwähnen, dass er DI persönlich kennt. Interfaces hat man sicher auch schon vor den DI Frameworks in Java mal im Constructor übergeben.

    volkard schrieb:

    zu den unit tests sagt er nichts, weil er seine testgetriebene zeit hinter sich hat.

    volkard programmiert auch nicht in einem Team mit 50 anderen die seinen Code kaputt machen können. Stimmts?



  • Bevor wir über die historischen Wurzeln der DI diskutieren und ob man das in C++ nicht schon lange vor Java gemacht hat, würde ich es sinnvoller finden, wenn wir diskutieren, ob IoC mit Templates oder IoC mit Interfaces (shared_ptr, RAW ptr usw.) der Vorzug zu geben ist. Eventuell gibt es garnicht das "beste" Verfahren in C++, sondern der Einsatz ist je nach Fall anhand bestimmter Kriterien auszuwählen.
    Hat hierzu jemand eine Meinung bzw. Erfahrungswerte?



  • Soweit ich das überblicke, handelt es sich bei dependency injection im Wesentlichen um eine Art eingeschränkte strategy pattern - eingeschränkt insofern, als dass man einen Haufen Factories drumherum baut, um die Strategien an zentraler Stelle festschreiben zu können. Dann kann man, indem man seine Factories entsprechend zusammensetzt, quasi eine Art Programmkonfiguration vornehmen. Guice und vergleichbare Frameworks nehmen einem dann die Arbeit ab, den Haufen Factories selbst schreiben zu müssen.

    So etwas kann man in C++ natürlich auch machen, es stellt sich aber ein bisschen die Frage, warum man es wollen sollte. Man müsste sämtliche Objekte per Referenz (bzw. Zeiger) verknüpfen, weil alles dynamisch erstellt werden müsste (am Ende wahrscheinlich sogar per shared_ptr, weil die Inhaberverhältnisse mitunter nicht-trivial zu klären sein dürften), und am Ende wäre der Gewinn der ganzen Angelegenheit einer, der sich nicht in der eigentlichen Funktionsweise des Programms niederschlägt. Wenn man ein bis zwei VMs von der Maschine weg ist, mag einen der Abstraktionsmalus nicht interessieren; mir, der ich hauptsächlich in C++ unterwegs bin und schon Heap-Allokation vermeide, wo es sinnvoll geht, erscheint das Vorhaben ziemlich wahnwitzig. Wenn man Java schreiben will, sollte man es nicht in C++ tun.

    Ob man das Ganze templatisiert aufziehen kann, ist eine interessante aber akademische Frage. Wenn ich bereit bin, die Injection zur Compilezeit zu betreiben, kann ich auch stumpf #ifdef schreiben oder für entsprechende Testfälle eine andere Implementation der auszutauschenden Klasse mitlinken.

    Damit sei übrigens nichts gegen die Strategy-Pattern gesagt - die ist oft sehr nützlich. Ihr Sinn erschließt sich mir aber nur, wenn man die Möglichkeit braucht, geeignete Strategien je nach Datenlage auszuwählen.

    Übrigens: Dependency-Injection als tolle Idee zum Testen von Code zu verkaufen, scheint mir etwas kurz gedacht. Zwar lassen sich (wie bei der Strategy-Pattern) entsprechend gebaute Client-Klassen leicht mit Mock-Objekten versehen, was in der Tat nützlich sein kann, wenn sie mit einer Außenwelt kommunizieren, die in der Testumgebung nicht vorhanden ist, man führt aber durch die Verwendung der Dependency-Injection neue Sachverhalte in den Code ein, die ebenfalls getestet werden müssen - etwa, dass in der Anwendung schlussendlich keine Mock-Objekte in den Factories stecken und dass die Factories alle richtig funktionieren. Es mag Zusammenhänge geben, in denen man da netto etwas gewinnen kann, aber darauf verlassen, dass es immer der Fall ist, würde ich mich nicht.

    Ansonsten erlaube ich mir mal, eine andere Herangehensweise an die Komponentenaustauscherei zu Testzwecken vorzuschlagen:

    class testable {
    public:
      testable()                       : backend_(backend_default_) { }
      testable(impl_base &replacement) : backend_(replacement)      { } // für Unit-Tests
    
      void do_something() {
        backend_.do_something();
      }
    
    private:
      impl       backend_default_;
      impl_base &backend_;
    };
    
    // Später, in Unit-Tests:
    
    mock_impl mock_backend;
    testable testee(mock_backend);
    
    testee.do_something();
    
    enforce(mock_backend.everything_worked());
    

    Das ist zwar nicht zero-overhead, aber zumindest hält sich der Mehraufwand in überschaubaren Grenzen.



  • seldon schrieb:

    Ob man das Ganze templatisiert aufziehen kann, ist eine interessante aber akademische Frage. Wenn ich bereit bin, die Injection zur Compilezeit zu betreiben, kann ich auch stumpf #ifdef schreiben oder für entsprechende Testfälle eine andere Implementation der auszutauschenden Klasse mitlinken.

    das führt aber den Sinn von DI ad absurdum, da du so keine sinnvollen Testfälle mehr bauen kannst. Im #define-Fall sind die Tests im Code der Klasse, im Linkerfall musst du hässlich neu linken. Templates haben diesen Nachteil nicht.



  • Äh...ich meine nicht, per #ifdef Testcode in den Klassen zu versenken, sondern den zu injectenden. Irgendwo muss der ja hin, und dafür extra ein Template-Gerüst zu bauen, das scheint mir ziemlich umständlich.

    So oder so scheint mir, wenn man so etwas tatsächlich betreiben will (Vorbehalte oben genannt), die Linkervariante sinnvoller. Letztendlich ist es ja die Aufgabe des Linkers, Abhängigkeiten aufzulösen, da kann man ihn auch dafür verwenden. Den Code, den man austauscht, kann man in den entsprechenden Testfällen eh nicht behandeln, also sollte es kein Problem darstellen, ihn nicht mitzulinken. Ggf. könnte man das mit ein bisschen Mühe sogar über den Laufzeitlinker regeln, aber allein zum Testen dürfte das Overkill sein. Und wenn man zur Laufzeit mal dieses, mal jenes Backend benutzen will, ist man eh nicht mehr bei dependency injection, sondern bei der vollen strategy pattern.

    Was du mit "hässlich neu linken" meinst, erschließt sich mir nicht. Es ist für mein Verständnis jedenfalls nicht hässlicher, als drei Factories pro Klasse einzuführen.



  • otze schrieb:

    das führt aber den Sinn von DI ad absurdum, da du so keine sinnvollen Testfälle mehr bauen kannst. Im #define-Fall sind die Tests im Code der Klasse, im Linkerfall musst du hässlich neu linken. Templates haben diesen Nachteil nicht.

    Im template fall muss man aber auch neu linken. Oder wie meinst du das?


Anmelden zum Antworten