it-swarm.com.de

Wie viel ist zu viel Abhängigkeitsinjektion?

Ich arbeite in einem Projekt, das (Spring) Dependency Injection für buchstäblich alles verwendet, was eine Abhängigkeit einer Klasse ist. Wir sind an einem Punkt angelangt, an dem die Spring-Konfigurationsdatei auf etwa 4000 Zeilen angewachsen ist. Vor nicht allzu langer Zeit habe ich einen von Onkel Bobs Vorträgen auf YouTube gesehen (leider konnte ich den Link nicht finden), in dem er empfiehlt, nur ein paar zentrale Abhängigkeiten (z. B. Fabriken, Datenbanken usw.) in die Hauptkomponente einzufügen, von der sie dann stammen verteilt werden.

Die Vorteile dieses Ansatzes liegen in der Entkopplung des DI-Frameworks vom größten Teil der Anwendung und machen die Spring-Konfiguration sauberer, da die Fabriken viel mehr von dem enthalten, was zuvor in der Konfiguration enthalten war. Im Gegenteil, dies führt dazu, dass die Erstellungslogik auf viele Fabrikklassen verteilt wird und das Testen möglicherweise schwieriger wird.

Meine Frage ist also wirklich, welche anderen Vor- oder Nachteile Sie in dem einen oder anderen Ansatz sehen. Gibt es Best Practices? Vielen Dank für Ihre Antworten!

39
Antimon

Wie immer hängt es davon ab. Die Antwort hängt von dem Problem ab, das man zu lösen versucht. In dieser Antwort werde ich versuchen, einige gemeinsame Motivationskräfte anzusprechen:

Bevorzugen Sie kleinere Codebasen

Wenn Sie 4.000 Zeilen Spring-Konfigurationscode haben, hat die Codebasis vermutlich Tausende von Klassen.

Es ist kaum ein Problem, das Sie nachträglich ansprechen können, aber als Faustregel bevorzuge ich eher kleinere Anwendungen mit kleineren Codebasen. Wenn Sie sich für Domain-Driven Design interessieren, können Sie beispielsweise eine Codebasis pro begrenztem Kontext erstellen.

Ich stütze diesen Rat auf meine begrenzte Erfahrung, da ich den größten Teil meiner Karriere webbasierten Branchencode geschrieben habe. Ich könnte mir vorstellen, dass es schwieriger ist, Dinge auseinander zu ziehen, wenn Sie eine Desktop-Anwendung, ein eingebettetes System oder ein anderes entwickeln.

Obwohl mir klar ist, dass dieser erste Rat leicht der am wenigsten praktische ist, glaube ich auch, dass er der wichtigste ist, und deshalb schließe ich ihn ein. Die Komplexität des Codes variiert nicht linear (möglicherweise exponentiell) mit der Größe der Codebasis.

Bevorzugen Sie Pure DI

Obwohl mir immer noch klar ist, dass diese Frage eine bestehende Situation darstellt, empfehle ich Pure DI . Verwenden Sie keinen DI-Container, aber wenn Sie dies tun, verwenden Sie ihn zumindest, um eine konventionelle Komposition zu implementieren .

Ich habe keine praktischen Erfahrungen mit Spring, gehe aber davon aus, dass durch die Konfigurationsdatei eine XML-Datei impliziert wird.

Das Konfigurieren von Abhängigkeiten mithilfe von XML ist das Schlimmste in beiden Welten. Erstens verlieren Sie die Sicherheit beim Kompilieren, aber Sie gewinnen nichts. Eine XML-Konfigurationsdatei kann leicht so groß sein wie der Code, den sie zu ersetzen versucht.

Im Vergleich zu dem Problem, das angesprochen werden soll, befinden sich Konfigurationsdateien für die Abhängigkeitsinjektion an der falschen Stelle auf der Konfigurationskomplexitätsuhr .

Der Fall für eine grobkörnige Abhängigkeitsinjektion

Ich kann mich für eine grobkörnige Abhängigkeitsinjektion aussprechen. Ich kann mich auch für eine feinkörnige Abhängigkeitsinjektion aussprechen (siehe nächster Abschnitt).

Wenn Sie nur einige "zentrale" Abhängigkeiten einfügen, sehen die meisten Klassen möglicherweise folgendermaßen aus:

public class Foo
{
    private readonly Bar bar;

    public Foo()
    {
        this.bar = new Bar();
    }

    // Members go here...
}

Dies passt immer noch zu Design Patterns 's bevorzugt die Objektzusammensetzung gegenüber der Klassenvererbung , da Foo komponiert Bar. Unter dem Gesichtspunkt der Wartbarkeit kann dies immer noch als wartbar angesehen werden, da Sie, wenn Sie die Zusammensetzung ändern müssen, einfach den Quellcode für Foo bearbeiten.

Dies ist kaum weniger wartbar als die Abhängigkeitsinjektion. Tatsächlich würde ich sagen, dass es einfacher ist, die Klasse, die Bar verwendet, direkt zu bearbeiten, als der Indirektion folgen zu müssen, die mit der Abhängigkeitsinjektion verbunden ist.

In der ersten Ausgabe von mein Buch über Abhängigkeitsinjektion Unterscheide ich zwischen flüchtigen und stabilen Abhängigkeiten.

Flüchtige Abhängigkeiten sind die Abhängigkeiten, die Sie in Betracht ziehen sollten. Sie beinhalten

  • Abhängigkeiten, die nach der Kompilierung neu konfiguriert werden müssen
  • Abhängigkeiten, die parallel von einem anderen Team entwickelt wurden
  • Abhängigkeiten mit nicht deterministischem Verhalten oder Verhalten mit Nebenwirkungen

Stabile Abhängigkeiten sind dagegen Abhängigkeiten, die sich genau definiert verhalten. In gewissem Sinne könnte man argumentieren, dass diese Unterscheidung für eine grobkörnige Abhängigkeitsinjektion spricht, obwohl ich zugeben muss, dass ich das beim Schreiben des Buches nicht ganz erkannt habe.

Aus Testsicht erschwert dies jedoch das Testen von Einheiten. Sie können Foo nicht mehr unabhängig von Bar testen. As J.B. Rainsberger erklärt , Integrationstests leiden unter einer kombinatorischen Explosion der Komplexität. Sie müssen buchstäblich Zehntausende von Testfällen schreiben, wenn Sie alle Pfade durch die Integration von sogar 4-5 Klassen abdecken möchten.

Das Gegenargument dazu ist, dass Ihre Aufgabe oft nicht darin besteht, eine Klasse zu programmieren. Ihre Aufgabe ist es, ein System zu entwickeln, das einige spezifische Probleme löst. Dies ist die Motivation hinter Behavior-Driven Development (BDD).

Eine andere Ansicht dazu präsentiert DHH, die behauptet, dass TDD zu testinduzierten Konstruktionsschäden führt . Er bevorzugt auch grobkörnige Integrationstests.

Wenn Sie diese Perspektive auf die Softwareentwicklung einnehmen, ist eine grobkörnige Abhängigkeitsinjektion sinnvoll.

Der Fall für eine feinkörnige Abhängigkeitsinjektion

Eine feinkörnige Abhängigkeitsinjektion könnte andererseits als Injektion aller Dinge beschrieben werden!

Mein Hauptanliegen bezüglich der grobkörnigen Abhängigkeitsinjektion ist die Kritik von J. B. Rainsberger. Sie können nicht alle Codepfade mit Integrationstests abdecken, da Sie buchstäblich Tausende oder Zehntausende von Testfällen schreiben müssen, um alle Codepfade abzudecken.

Die Befürworter von BDD kontern mit dem Argument, dass Sie nicht alle Codepfade mit Tests abdecken müssen. Sie müssen nur diejenigen abdecken, die geschäftlichen Wert erzeugen.

Nach meiner Erfahrung werden jedoch alle "exotischen" Codepfade auch in einer Bereitstellung mit hohem Volumen ausgeführt. Wenn sie nicht getestet werden, weisen viele dieser Pfade Fehler auf und verursachen Laufzeitausnahmen (häufig Nullreferenzausnahmen).

Dies hat mich veranlasst, eine feinkörnige Abhängigkeitsinjektion zu bevorzugen, da ich damit die Invarianten aller Objekte isoliert testen kann.

Bevorzugen Sie die funktionale Programmierung

Während ich mich zur feinkörnigen Abhängigkeitsinjektion neige, habe ich meinen Schwerpunkt auf funktionale Programmierung verlagert, unter anderem weil es ist an sich testbar .

Je mehr Sie sich in Richtung SOLID Code bewegen, desto funktionaler wird es . Früher oder später können Sie auch den Sprung wagen. Funktionale Architektur ist Ports und Adapter Architektur , und Abhängigkeitsinjektion ist auch ein Versuch und Ports und Adapter . Der Unterschied besteht jedoch darin, dass eine Sprache wie Haskell diese Architektur über ihr Typsystem erzwingt.

Bevorzugen Sie statisch typisierte funktionale Programmierung

An diesem Punkt habe ich die objektorientierte Programmierung (OOP) im Wesentlichen aufgegeben, obwohl viele der Probleme von OOP mehr als das Konzept selbst an Mainstream-Sprachen wie Java und C # gekoppelt sind.

Das Problem mit den gängigen OOP Sprachen ist, dass es nahezu unmöglich ist, das kombinatorische Explosionsproblem zu vermeiden, das ungetestet zu Laufzeitausnahmen führt. Statisch typisierte Sprachen wie Haskell und F # ermöglichen es Ihnen andererseits, viele Entscheidungspunkte im Typsystem zu codieren. Dies bedeutet, dass der Compiler Ihnen nicht Tausende von Tests schreiben muss, sondern nur sagt, ob Sie sich mit allen möglichen Codepfaden befasst haben (bis zu einem gewissen Grad; es ist kein Wundermittel).

Außerdem ist Abhängigkeitsinjektion nicht funktionsfähig . Echte funktionale Programmierung muss den gesamten Begriff der Abhängigkeiten ablehnen . Das Ergebnis ist einfacher Code.

Zusammenfassung

Wenn ich gezwungen bin, mit C # zu arbeiten, bevorzuge ich eine feinkörnige Abhängigkeitsinjektion, da ich damit die gesamte Codebasis mit einer überschaubaren Anzahl von Testfällen abdecken kann.

Am Ende ist meine Motivation schnelles Feedback. Dennoch ist Unit-Test nicht der einzige Weg, um Feedback zu erhalten .

45
Mark Seemann

Riesige DI-Setup-Klassen sind ein Problem. Aber denken Sie an den Code, den er ersetzt.

Sie müssen all diese Dienste und so weiter instanziieren. Der Code dazu befindet sich entweder im Startpunkt app.main() und wird manuell injiziert oder als this.myService = new MyService(); innerhalb der Klassen eng gekoppelt.

Reduzieren Sie die Größe Ihrer Setup-Klasse, indem Sie sie in mehrere Setup-Klassen aufteilen und vom Programmstartpunkt aus aufrufen. dh.

main()
{
   var c = new diContainer();
   var service1 = diSetupClass.SetupService1(c);
   var service2 = diSetupClass.SetupService2(c, service1); //if service1 is required by service2
   //etc

   //main logic
}

Sie müssen nicht auf service1 oder 2 verweisen, außer um sie an andere ci-Setup-Methoden zu übergeben.

Dies ist besser als der Versuch, sie aus dem Container abzurufen, da dadurch die Reihenfolge des Aufrufs der Setups erzwungen wird.

1
Ewan