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



  • 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