Welche Komplexität fügen DI-Frameworks hinzu?

8

Die derzeit am besten bewertete Antwort auf eine sehr aktuelle Frage besagt dies

DI-Container sind ein "Unternehmenssoftware" -Muster, das verwendet wird, wenn das Objektdiagramm sehr groß und komplex ist. Ich vermute, dass 95% der Anwendungen dies nicht erfordern.

Dem stimme ich überhaupt nicht zu. Vielleicht habe ich die Terminologie falsch verstanden, aber für mich bedeutet DI-Framework nur "etwas, das meine Objekte miteinander verbindet". Vermisse ich etwas

Ich benutze Guice sogar für wirklich kleine Projekte (wie 10 Klassen), um sie zu vereinfachen . Sicher, es ist eine 400-kB-JAR-Datei, aber das ist mir egal. Für ein kleines Projekt brauche ich kaum eine Konfiguration und der einzige "Overhead" ist das Hinzufügen der @InjectAnmerkung.

Ich frage mich also wirklich, welche zusätzliche Komplexität DI-Frameworks verursachen.

Update Adressierung der Antworten

In einem 82-Klassen-Projekt habe ich

  • 32 @InjectAnmerkungen
  • 15 @Singletonund 1 @ProvidedByAnmerkungen
  • 4 Anbieter (alle würde ich auch ohne DI brauchen, da sie meine Fabriken sind)
  • 1 Modul mit einer einzelnen Zeile
  • 0 Zeilen XML !!!

Das ist alles. Sicher, es ist ein kleines Projekt, aber genau das war mein Punkt. Die zusätzliche Arbeit bestand eher aus ein paar Worten als aus Zeilen .

Ich verwende ausschließlich Konstruktorinjektion, um unveränderliche "unbeschwerte" Objekte zu erhalten. Immer wenn eine neue Abhängigkeit auftritt, füge ich ein letztes Feld hinzu, lasse Lomboks RequiredArgsConstructor sich um die Deklaration kümmern und lasse Guice sich darum kümmern, sie ordnungsgemäß aufzurufen.

Maaartinus
quelle
macht Guice eine Eigenschaftsinjektion?
Reactgular
@MathewFoscarini Ich denke schon - ich kenne andere Begriffe und dies sollte (ein Sonderfall von) der Methodeninjektion sein.
Maaartinus
1
Ich bin so dankbar für Guice. Es hat meine Entwicklung stark vereinfacht und sie testbarer und robuster gemacht. Ich kann nicht glauben, dass ich so lange ohne DI war. Der Kuchen ist echt.
Der Koordinator

Antworten:

6

Soweit ich sehen kann, müssen bei der Verwendung von DI-Frameworks einige Kompromisse eingegangen werden.

Das Besorgniserregendste für mich ist, dass Ihr Anwendungscode normalerweise zwischen einer Hauptprogrammiersprache (wie Java) und XML / JSON-Konfigurationscode (es ist Code) verteilt ist. Dies bedeutet, dass Sie bei Problemen mit Ihrer Anwendung an zwei Stellen suchen müssen. Oft ist der Konfigurationscode nicht leicht mit dem Hauptanwendungscode zu verknüpfen.

Natürlich liegt die Konfiguration auch außerhalb der Grenzen dessen, was der Compiler (und häufig die IDE) für Sie überprüfen kann, was bedeutet, dass es viel einfacher ist, Fehler in dieser Konfiguration zu machen, als wenn Sie eine Hauptklasse schreiben, die die Verkabelung handhabt. Tatsächlich bedeutet dies, dass Verkabelungsprobleme von Problemen zur Kompilierungszeit zu Laufzeitproblemen werden.

Neben der Aufteilung der Anwendung bedeutet die Verwendung von DI-Frameworks häufig auch, dass Sie @Injectanstelle herkömmlicher DI-Techniken (CTOR-Schnittstelleninjektion in Java / C ++, Vorlageninjektion in C ++) innerhalb Ihres Codes oder ähnliches verwenden. Die Kehrseite der Medaille ist , dass Sie müssen dann einen DI - Framework mit der Anwendung verwenden. Eine Alternative wäre, Ihre Anwendung zu entwerfen, ohne ein DI-Framework zu erwarten, und dann dem DI-Framework zu erlauben, die traditionelle CTOR / Setter-Injektion Ihrer Klassen wiederzuverwenden.

Der Nachteil des herkömmlichen Injektionsmechanismus besteht darin, dass eine komplexe und ordnungsgemäß eingekapselte Klasse zur CTOR-Zeit eine Reihe anderer relativ komplexer Injektionen erfordert. Dies wird normalerweise durch Einarbeiten von a gelöst Builder, aber es kann ein Schmerz sein.

BEARBEITEN :

Die Jungs unten haben erwähnt, dass Guicekeine separate XML / JSON-Konfiguration erforderlich ist. Meine Antwort bezieht sich wirklich auf meine eigene Verwendung von Spring.

Dennis
quelle
2
Das Schöne an Guice ist, dass es vollständig in Java konfiguriert werden kann (es gibt kein XML, es sei denn, Sie erhalten ein Add-On). Für kleine Projekte (worüber ich frage) reicht eigentlich die "automatische Verkabelung" aus. Ich habe meine Frage aktualisiert. Vielleicht kommen alle Ängste von schlechten (oder zu unternehmerischen DI-Frameworks)?
Maaartinus
1
@ Tennis: Guice ist überhaupt nicht aufdringlich. Der Guice-Container ist lediglich eine alternative Methode zum Abrufen von Objektinstanzen. Sie können weiterhin Konstruktoren aufrufen, wenn Sie möchten. Dies ist praktisch für Unit-Tests mit verspotteten Abhängigkeiten.
Kevin Cline
1
Ich kann nicht besonders mit Guice sprechen, aber viele DI-Stapel sind sehr automatisch, wenn Sie sie nur für Dinge verwenden, die lange Lebenszyklen haben - Singletons, Fabriken und dergleichen. Mit diesen Objekttypen mit Konstruktorinjektion können Sie sich ganz einfach von statischen Dingen und harten Abhängigkeiten lösen. und ich finde es nützlich für diese Fälle, auch für winzige Projekte. Ich würde sagen, wenn Sie die einfacheren Funktionen und die Konstruktorinjektion eines DI-Frameworks verwenden und dies eine Menge Komplexität hinzufügt ... finden Sie ein neues DI-Framework.
Danwyand
1
@maaartinus - Da könntest du recht haben. Um ehrlich zu sein, kenne ich Guice nicht. Ich nehme an, dass meine Antwort hauptsächlich für den Frühling gilt.
Dennis
4

Ich denke, der Hauptnachteil der Verwendung eines IoC-Containers ist die Magie, die er hinter den Kulissen ausübt. (Dies ist auch ein Problem bei ORMs, einem weiteren gängigen magischen Tool.) Das Debuggen von IoC-Problemen macht keinen Spaß, da es schwieriger wird zu sehen, wie und wann Abhängigkeiten erfüllt werden. Sie können die Abhängigkeitsauflösung im Debugger nicht schrittweise durchlaufen.

Container machen es oft umständlich, "Klasse A mit einer Version einer Abhängigkeit und Klasse B mit einer anderen Version" oder ähnliche Szenarien zu konfigurieren, in denen Abstraktionen wiederverwendet werden. Sie können in Situationen geraten, in denen jede Klasse ihre eigene Schnittstelle hat, auch wenn sie dasselbe bedeuten. Dies blockiert Sie von der Abstraktionskraft der Wiederverwendung von Schnittstellen.

Meiner Meinung nach ist der automatische Gewinn, den Sie mit IoC erzielen, das automatische Lifestyle-Management. Sie können Komponenten als Singletons, Singletons pro Anforderung, transiente Instanzen usw. deklarieren - und der Container erledigt alles für Sie. Es ist erheblich schwieriger, dies mit new-expressions einzurichten . Auf der anderen Seite ist es immer noch leicht, Ressourcenlecks zu verursachen, indem Lebensstile nicht übereinstimmen.

Einige Projekte verwenden IoC, um alles im System - sogar Klassen, die Geschäftsdaten darstellen - ohne newSchlüsselwörter zu erstellen . Ich denke, dies ist im Allgemeinen ein Fehler, da es Sie dazu ermutigt, schlecht gekapselte Klassen und zu viele Schnittstellen zu schreiben. Ihr Code wird abhängig von IoC. Es scheint auch komorbid mit Übertests zu sein.

Ich persönlich ziehe es im Allgemeinen vor, auf einen Container zu verzichten. Stattdessen komponiere ich Objekte mit new-expressions, die in einem Kompositionsstamm eingekapselt sind. Es ist leicht, die konkreten Abhängigkeiten einer bestimmten Klasse zu erkennen, und wenn ich einen Konstruktor ändere, erhalte ich Fehler bei der Kompilierung, die beide große Wartungsgewinne bedeuten. Es ist auch einfach, separate Instanzen mit unterschiedlichen Komponenten zu konfigurieren.

Mein Rat, wenn Sie einen IoC-Container verwenden möchten, ist, Ihre Abhängigkeiten am Einstiegspunkt in das System zu erfüllen und sie nur für Ihre tatsächlichen Abhängigkeiten zu verwenden (Repositorys et al.). Denken Sie daran, dass Sie die Abhängigkeitsinjektion weiterhin verwenden und die Konfiguration sogar in einer einzelnen Datei beibehalten können, ohne einen Container zu verwenden.

Benjamin Hodgson
quelle
1
"Denken Sie daran, dass Sie die Abhängigkeitsinjektion weiterhin verwenden und die Konfiguration sogar in einer einzigen Datei behalten können, ohne einen Container zu verwenden." ++. Ich denke, zu viele Leute vergessen, dass DI- und DI-Container-Frameworks nicht dasselbe sind.
RubberDuck
1

Einige Entwickler argumentieren, dass jedes Framework, das Sie in Ihrem Code verwenden, zu einem späteren Zeitpunkt ein potenzielles Problem darstellt, da Sie normalerweise Ihren Code auf eine bestimmte Weise schreiben müssen, um korrekt mit ihm zu interagieren. Dies kann zu einem späteren Zeitpunkt zu Problemen führen Es stellt sich heraus, dass diese Methoden im Widerspruch zu anderen Designanforderungen stehen. Ein Beispiel für diese Sorge wird von Bob Martin ausgedrückt hier . Ich persönlich stimme nicht zu, aber ich kann seinen Standpunkt sehen.

Jules
quelle
2
Die Vorteile, die sich aus der Verwendung dieses Frameworks in einem komplexeren Projekt ergeben, können das Risiko wert sein?
JeffO
1

Zusammenfassend lässt sich sagen, dass die einzige Komplexität darin besteht, falsche Konfigurationsmethoden zu verwenden (ja, XML ist nicht typsicher) und möglicherweise auch aus Frameworks, die viel mehr Arbeit leisten (Spring verwaltet den Objektlebenszyklus, der weit über DI hinausgeht).

Für ein einfaches Projekt ist es äußerst nützlich, DI richtig zu machen:

  • Es ermöglicht das Schreiben von Diensten als winzige unveränderliche Klassen mit minimalem Aufwand
  • Es stellt die Singletonness im richtigen Bereich sicher und bleibt gleichzeitig testbar
  • es braucht kaum Konfiguration (ca. 1 Zeile pro 20 DI-Klassen)

Es ist auch ziemlich harmlos, da alles manuell gemacht werden könnte. Es geht nur darum, die Instanzen zu erstellen, sie an Konstruktoren zu übergeben, um mehr Instanzen zu erstellen, und so weiter. Langweilig und lang, aber einfach.

Ein typischer Service mit Guice und Lombok sieht so aus

@Singleton
@RequiredArgsConstructor(onConstructor=@__(@Inject))
public class TotalCostsComputer {
    public CostsResult getCosts(Goods goods, Shippment shippment) {
        return taxComputer.applyTax(composeResult(
                    goodsCostComputer.getCosts(goods),
                    shippmentCostComputer.getCosts(shippment));
    }
    ...

    private final GoodsCostComputer goodsCostComputer;
    private final ShippmentCostComputer shippmentCostComputer;
    private final TaxComputer taxComputer;
}

Wenn Sie nun eine Abhängigkeit hinzufügen, müssen Sie lediglich ein neues privates Endfeld hinzufügen . Es kann nicht null sein (Guice garantiert dies auch ohne @NonNull ).

Testen bedeutet, entweder eigene Instanzen zu erstellen oder diese von der Injectorzu beziehen (möglicherweise so konfiguriert, dass einige Stubs zurückgegeben werden). Beides ist ziemlich einfach.

Maaartinus
quelle