Wie immer ist It Depends ™. Die Antwort hängt von dem Problem ab, das gelöst werden soll. In dieser Antwort werde ich versuchen, einige gemeinsame motivierende Kräfte anzusprechen:
Bevorzugen Sie kleinere Codebasen
Wenn Sie über 4.000 Zeilen Spring-Konfigurationscode verfügen, besteht die Codebasis vermutlich aus Tausenden von Klassen.
Es ist kaum ein Thema, das Sie nachträglich ansprechen können, aber als Faustregel bevorzuge ich kleinere Anwendungen mit kleinerer Codebasis. Wenn Sie sich für domänenbasiertes Design interessieren , können Sie beispielsweise eine Codebasis für einen begrenzten Kontext erstellen.
Dieser Rat basiert auf meiner begrenzten Erfahrung, da ich den größten Teil meiner Karriere über webbasierten Code für Geschäftsbereiche geschrieben habe. Ich könnte mir vorstellen, dass es schwieriger ist, Dinge auseinander zu ziehen, wenn Sie eine Desktop-Anwendung, ein eingebettetes System oder anderes entwickeln.
Obwohl mir klar ist, dass dieser erste Rat einfach der unpraktischste 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 zum Implementieren einer auf Konventionen basierenden Komposition .
Ich habe keine praktischen Erfahrungen mit Spring, gehe aber davon aus, dass per Konfigurationsdatei eine XML-Datei impliziert wird.
Die Konfiguration von Abhängigkeiten mithilfe von XML ist die schlimmste beider Welten. Erstens verlieren Sie die Sicherheit beim Kompilieren, aber Sie gewinnen nichts. Eine XML-Konfigurationsdatei kann problemlos so groß sein wie der Code, den sie zu ersetzen versucht.
Verglichen mit dem Problem, das behoben werden soll, belegen Konfigurationsdateien für die Abhängigkeitsinjektion den falschen Platz in der Uhr für die Komplexität der Konfiguration .
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 ein paar "zentrale" Abhängigkeiten einfügen, sehen die meisten Klassen möglicherweise so aus:
public class Foo
{
private readonly Bar bar;
public Foo()
{
this.bar = new Bar();
}
// Members go here...
}
Dies ist immer noch paßt Design Patterns ‚s für Objekt - Komposition über Klassenvererbung , da Foo
komponiert Bar
. Aus Sicht der Wartbarkeit kann dies immer noch als wartbar angesehen werden, da Sie den Quellcode einfach bearbeiten müssen, wenn Sie die Komposition ändern müssen Foo
.
Dies ist kaum weniger wartbar als die Abhängigkeitsinjektion. Tatsächlich würde ich sagen, dass es einfacher ist, die verwendete Klasse direkt zu bearbeiten, als der Bar
mit der Abhängigkeitsinjektion verbundenen Indirektion zu folgen.
In der ersten Ausgabe meines Buches über Abhängigkeitsinjektion unterscheide ich zwischen flüchtigen und stabilen Abhängigkeiten.
Flüchtige Abhängigkeiten sind die Abhängigkeiten, die Sie einschleusen sollten. Sie beinhalten
- Abhängigkeiten, die nach der Kompilierung neu konfiguriert werden müssen
- Abhängigkeiten wurden parallel von einem anderen Team entwickelt
- Abhängigkeiten mit nicht deterministischem Verhalten oder Verhalten mit Nebenwirkungen
Stabile Abhängigkeiten hingegen sind 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 realisiert habe.
Aus der Sicht der Tests wird das Testen von Einheiten jedoch schwieriger. Sie können nicht mehr Foo
unabhängig von Unit-Test Bar
. Wie JB Rainsberger erklärt , leiden Integrationstests 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 häufig 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 hierzu vertritt DHH, die behauptet, TDD führe zu testbedingten Designschäden . Er bevorzugt auch grobkörnige Integrationstests.
Wenn Sie diese Perspektive für die Softwareentwicklung einnehmen, ist eine grobkörnige Abhängigkeitsinjektion sinnvoll.
Der Fall für eine feinkörnige Abhängigkeitsinjektion
Eine feinkörnige Abhängigkeitsinjektion hingegen könnte als " alle Dinge injizieren" bezeichnet werden!
Mein Hauptanliegen bezüglich der grobkörnigen Abhängigkeitsinjektion ist die von JB Rainsberger geäußerte Kritik. Sie können nicht alle Codepfade mit Integrationstests abdecken, da Sie buchstäblich Tausende oder Zehntausende Testfälle 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 Nutzen bringen.
Nach meiner Erfahrung werden jedoch alle "exotischen" Codepfade auch in einer Bereitstellung mit hohem Volumen ausgeführt, und wenn sie nicht getestet werden, weisen viele davon Fehler auf und verursachen Laufzeitausnahmen (häufig Ausnahmen mit Nullreferenz).
Dies hat mich veranlasst, eine feinkörnige Abhängigkeitsinjektion zu bevorzugen, da ich damit die Invarianten aller Objekte isoliert testen kann.
Funktionale Programmierung bevorzugen
Während ich mich für eine feinkörnige Abhängigkeitsinjektion interessiere, habe ich meinen Schwerpunkt auf die funktionale Programmierung verlagert, unter anderem, weil sie an sich testbar ist .
Je mehr Sie sich in Richtung SOLID-Code bewegen, desto funktionaler wird dieser . Früher oder später können Sie auch den Sprung wagen. Die funktionale Architektur ist die Architektur von Ports und Adaptern , und die Abhängigkeitsinjektion ist auch ein Versuch, Ports und Adapter zu verwenden . Der Unterschied besteht jedoch darin, dass eine Sprache wie Haskell diese Architektur über ihr Typensystem erzwingt.
Bevorzugen Sie statisch typisierte funktionale Programmierung
An diesem Punkt habe ich im Wesentlichen die objektorientierte Programmierung (OOP) aufgegeben, obwohl viele der Probleme von OOP mehr mit den gängigen Sprachen wie Java und C # als mit dem eigentlichen Konzept verknüpft sind.
Das Problem mit den gängigen OOP-Sprachen ist, dass das Problem der kombinatorischen Explosion, das, ungetestet, zu Laufzeitausnahmen führt, so gut wie unmöglich zu vermeiden ist. Mit statisch getippten Sprachen wie Haskell und F # können Sie dagegen viele Entscheidungspunkte im Typensystem codieren. Dies bedeutet, dass der Compiler Ihnen nicht Tausende von Tests schreiben muss, sondern lediglich mitteilt, ob Sie sich mit allen möglichen Codepfaden befasst haben (bis zu einem gewissen Grad, es ist keine Silberkugel).
Außerdem ist die Abhängigkeitsinjektion nicht funktionsfähig . Wahre funktionale Programmierung muss den gesamten Begriff der Abhängigkeiten verwerfen . Das Ergebnis ist einfacherer Code.
Zusammenfassung
Wenn ich gezwungen bin, mit C # zu arbeiten, bevorzuge ich die feinkörnige Abhängigkeitsinjektion, da ich so die gesamte Codebasis mit einer überschaubaren Anzahl von Testfällen abdecken kann.
Am Ende ist meine Motivation schnelles Feedback. Dennoch ist Unit - Tests nicht die einzige Möglichkeit , Feedback zu bekommen .