Was ist der "richtige" Weg, DI in .NET zu implementieren?


22

Ich versuche, die Abhängigkeitsinjektion in einer relativ großen Anwendung zu implementieren, habe jedoch keine Erfahrung damit. Ich habe das Konzept und einige verfügbare Implementierungen von IoC- und Abhängigkeitsinjektoren wie Unity und Ninject studiert. Es gibt jedoch eine Sache, die mir entgeht. Wie soll ich die Instanzerstellung in meiner Anwendung organisieren?

Was ich überlege, ist, dass ich einige bestimmte Fabriken erstellen kann, die die Logik zum Erstellen von Objekten für einige bestimmte Klassentypen enthalten. Grundsätzlich eine statische Klasse mit einer Methode, die die Ninject Get () - Methode einer statischen Kernel-Instanz in dieser Klasse aufruft.

Wird es ein korrekter Ansatz für die Implementierung der Abhängigkeitsinjektion in meiner Anwendung sein, oder sollte ich ihn nach einem anderen Prinzip implementieren?


5
Ich glaube nicht, dass es den richtigen Weg gibt, aber viele richtige Wege, abhängig von Ihrem Projekt. Ich würde mich an die anderen halten und eine Konstruktorinjektion vorschlagen, da Sie sicherstellen können, dass jede Abhängigkeit an einem einzelnen Punkt injiziert wird. Wenn die Konstruktorsignaturen zu lang werden, wissen Sie, dass die Klassen zu viel bewirken.
Paul Kertscher

Es ist schwer zu beantworten, ohne zu wissen, was für ein .net-Projekt Sie erstellen. Eine gute Antwort für WPF könnte zum Beispiel eine schlechte Antwort für MVC sein.
JMK

Es ist schön, alle Abhängigkeitsregistrierungen in einem DI-Modul für die Lösung oder für jedes Projekt zu organisieren, und möglicherweise eines für einige Tests, je nachdem, was Sie ausführlich testen möchten. Oh ja, natürlich solltest du Konstruktor-Injection verwenden, das andere Zeug ist für fortgeschrittene / verrückte Verwendungen.
Mark Rogers

Antworten:


30

Denken Sie noch nicht an das Tool, das Sie verwenden werden. Sie können DI ohne einen IoC-Container ausführen.

Erster Punkt: Mark Seemann hat ein sehr gutes Buch über DI in .Net

Zweitens: Kompositionswurzel. Stellen Sie sicher, dass die gesamte Einrichtung am Einstiegspunkt des Projekts erfolgt ist. Der Rest Ihres Codes sollte sich mit Injektionen auskennen und nicht mit den verwendeten Tools.

Drittens: Konstruktoreinspritzung ist der wahrscheinlichste Weg (es gibt Fälle, in denen Sie es nicht wollen, aber nicht so viele).

Viertens: Versuchen Sie, Lambda-Fabriken und ähnliche Funktionen zu verwenden, um zu vermeiden, dass nicht benötigte Interfaces / Klassen nur zum Zweck der Injektion erstellt werden.


5
Alles ausgezeichnete Ratschläge; Vor allem der erste Teil: Lernen Sie, wie man reine DIs erstellt, und beginnen Sie dann mit der Suche nach IoC-Containern, die die Menge des für diesen Ansatz erforderlichen Boilerplate-Codes reduzieren.
David Arno

6
... oder IoC-Container überspringen, um alle Vorteile der statischen Überprüfung zu nutzen.
Den

Ich finde, dieser Rat ist ein guter Ausgangspunkt, bevor ich zu DI komme, und ich habe tatsächlich das Buch von Mark Seemann.
Snoop

Ich kann diese Antwort unterstützen. In einer ziemlich großen Anwendung haben wir erfolgreich den handgeschriebenen Poor-Mans-DI (Bootstrapper) verwendet, indem wir Teile der Bootstrapper-Logik in die Module zerlegt haben, aus denen die Anwendung bestand.
Perücke

1
Achtung. Die Verwendung von Lambda-Injektionen kann ein schneller Weg zum Wahnsinn sein, insbesondere bei testbedingten Designschäden. Ich kenne. Ich bin diesen Weg gegangen.
jpmc26

13

Ihre Frage besteht aus zwei Teilen: der richtigen Implementierung von DI und der Umgestaltung einer großen Anwendung für die Verwendung von DI.

Der erste Teil wird von @Miyamoto Akira gut beantwortet (insbesondere die Empfehlung, Mark Seemanns Buch "Dependency Injection in .net" zu lesen. Marks Blog ist auch eine gute kostenlose Ressource.

Der zweite Teil ist um einiges komplizierter.

Ein guter erster Schritt wäre, einfach alle Instanziierungen in die Klassenkonstruktoren zu verschieben - ohne die Abhängigkeiten zu injizieren, sondern nur sicherzustellen, dass Sie nur newden Konstruktor aufrufen .

Dadurch werden alle SRP-Verstöße hervorgehoben, die Sie begangen haben, sodass Sie die Klasse in kleinere Mitarbeiter aufteilen können.

Die nächste Ausgabe, die Sie finden werden, sind Klassen, deren Konstruktion auf Laufzeitparametern basiert. Sie können dies in der Regel beheben, indem Sie einfache Factorys erstellen, häufig mit Func<param,type>, diese im Konstruktor initialisieren und in den Methoden aufrufen.

Der nächste Schritt besteht darin, Schnittstellen für Ihre Abhängigkeiten zu erstellen und Ihren Klassen einen zweiten Konstruktor hinzuzufügen, der diese Schnittstellen ausschließt. Ihr parameterloser Konstruktor würde die konkreten Instanzen neu erstellen und an den neuen Konstruktor übergeben. Dies wird allgemein als "B * stard Injection" oder "Poor mans DI" bezeichnet.

Dies gibt Ihnen die Möglichkeit, einige Unit-Tests durchzuführen, und wenn dies das Hauptziel des Refactors war, können Sie dort anhalten. Neuer Code wird mit Konstruktorinjektion geschrieben, aber Ihr alter Code kann weiterhin wie geschrieben funktionieren, ist jedoch noch testbar.

Sie können natürlich weiter gehen. Wenn Sie beabsichtigen, einen IOC-Container zu verwenden, besteht ein nächster Schritt möglicherweise darin, alle direkten Aufrufe newin Ihren parameterlosen Konstruktoren durch statische Aufrufe des IOC-Containers zu ersetzen und diesen im Wesentlichen (ab) als Service-Locator zu verwenden.

Dadurch werden mehr Fälle von Laufzeitkonstruktorparametern ausgelöst, die wie zuvor behandelt werden müssen.

Sobald dies erledigt ist, können Sie damit beginnen, die parameterlosen Konstruktoren zu entfernen und zu reinem DI umzugestalten.

Letztendlich wird dies eine Menge Arbeit sein, also stellen Sie sicher, dass Sie entscheiden, warum Sie es tun möchten, und priorisieren Sie die Teile der Codebasis, die am meisten vom Refactor profitieren


3
Vielen Dank für eine sehr ausführliche Antwort. Sie haben mir ein paar Ideen gegeben, wie ich mich dem Problem nähern kann. Die gesamte Architektur der App ist bereits auf IoC ausgelegt. Der Hauptgrund, warum ich DI verwenden möchte, ist nicht einmal das Testen von Einheiten, sondern vielmehr die Möglichkeit, verschiedene Implementierungen für verschiedene Schnittstellen, die im Kern meiner Anwendung definiert sind, mit möglichst geringem Aufwand auszutauschen. Die betreffende Anwendung funktioniert in einer sich ständig ändernden Umgebung, und ich muss häufig Teile der Anwendung austauschen, um neue Implementierungen entsprechend den Änderungen in der Umgebung zu verwenden.
User3223738

1
Ich bin froh, dass ich helfen konnte, und ich stimme zu, dass der größte Vorteil von DI die lose Kopplung und die damit verbundene einfache Rekonfigurierbarkeit ist, wobei Unit-Tests ein netter Nebeneffekt sind.
Steve

1

Zunächst möchte ich erwähnen, dass Sie sich dies erheblich erschweren, indem Sie ein vorhandenes Projekt umgestalten, anstatt ein neues Projekt zu starten.

Sie sagten, es sei eine große Anwendung, wählen Sie also zunächst eine kleine Komponente aus. Vorzugsweise eine "Blattknoten" -Komponente, die von nichts anderem verwendet wird. Ich weiß nicht, wie der Status der automatisierten Tests in dieser Anwendung ist, aber Sie brechen alle Komponententests für diese Komponente ab. Also sei darauf vorbereitet. In Schritt 0 werden Integrationstests für die zu ändernde Komponente geschrieben, sofern diese noch nicht vorhanden sind. Als letzte Möglichkeit (keine Testinfrastruktur; kein Buy-In zum Schreiben) sollten Sie eine Reihe manueller Tests durchführen, um zu überprüfen, ob diese Komponente funktioniert.

Die einfachste Möglichkeit, Ihr Ziel für den DI-Refactor anzugeben, besteht darin, alle Instanzen des Operators 'new' aus dieser Komponente zu entfernen. Diese fallen im Allgemeinen in zwei Kategorien:

  1. Invariante Mitgliedsvariable: Dies sind Variablen, die einmal festgelegt werden (normalerweise im Konstruktor) und für die Lebensdauer des Objekts nicht neu zugewiesen werden. Für diese können Sie eine Instanz des Objekts in den Konstruktor einfügen. Sie sind im Allgemeinen nicht für die Entsorgung dieser Objekte verantwortlich (ich möchte hier nicht sagen, niemals, aber Sie sollten diese Verantwortung wirklich nicht haben).

  2. Variant-Member-Variable / Methodenvariable: Dies sind Variablen, bei denen zu einem bestimmten Zeitpunkt während der Lebensdauer des Objekts Datenmüll gesammelt wird. In diesen Fällen möchten Sie Ihrer Klasse eine Factory hinzufügen, um diese Instanzen bereitzustellen. Sie sind für die Entsorgung von Objekten verantwortlich, die von einer Fabrik erstellt wurden.

Ihr IoC-Container (ninject, wie es sich anhört) übernimmt die Verantwortung für die Instanziierung dieser Objekte und die Implementierung Ihrer Factory-Schnittstellen. Was auch immer die Komponente verwendet, die Sie geändert haben, muss über den IoC-Container informiert sein, damit er Ihre Komponente abrufen kann.

Sobald Sie die oben genannten Schritte ausgeführt haben, können Sie alle Vorteile nutzen, die Sie von DI in Ihrer ausgewählten Komponente erhoffen. Jetzt wäre ein guter Zeitpunkt, um diese Komponententests hinzuzufügen / zu korrigieren. Wenn es Unit-Tests gegeben hat, müssen Sie entscheiden, ob Sie sie durch Injizieren realer Objekte zusammenfügen oder neue Unit-Tests mit Hilfe von Mocks schreiben möchten.

Wiederholen Sie einfach die obigen Schritte für jede Komponente Ihrer Anwendung, und verschieben Sie dabei den Verweis auf den IoC-Container nach oben, bis nur noch der Hauptteil davon wissen muss.


1
Guter Rat: Beginnen Sie mit einer kleinen Komponente anstatt mit dem 'BIG re-write'
Daniel Hollinrake

0

Richtiger Ansatz ist es, Konstruktorinjektion zu verwenden, wenn Sie verwenden

Was ich überlege, ist, dass ich einige bestimmte Fabriken erstellen kann, die die Logik zum Erstellen von Objekten für einige bestimmte Klassentypen enthalten. Grundsätzlich eine statische Klasse mit einer Methode, die die Ninject Get () - Methode einer statischen Kernel-Instanz in dieser Klasse aufruft.

Dann haben Sie den Service Locator als Abhängigkeitsinjektion.


Sicher. Konstruktorinjektion. Angenommen, ich habe eine Klasse, die eine Schnittstellenimplementierung als eines der Argumente akzeptiert. Ich muss jedoch noch eine Instanz der Schnittstellenimplementierung erstellen und an diesen Konstruktor übergeben. Vorzugsweise sollte es sich um einen zentralen Code handeln.
User3223738

2
Nein, wenn Sie den DI-Container initialisieren, müssen Sie die Implementierung von Schnittstellen angeben, und der DI-Container erstellt die Instanz und fügt sie dem Konstruktor hinzu.
Tieffliegender Pelikan

Persönlich empfinde ich Konstruktor-Injection als überstrapaziert. Ich habe es viel zu oft gesehen, dass 10 verschiedene Services injiziert werden und nur einer für einen Funktionsaufruf benötigt wird - warum ist das dann nicht Teil des Funktionsarguments?
Urbanhusky

2
Wenn 10 verschiedene Dienste eingespeist werden, liegt dies daran, dass jemand gegen die SRP verstößt, die in kleinere Komponenten aufgeteilt werden sollte.
Tieffliegender Pelikan

1
@Fabio Die Frage ist, was das dir kaufen würde. Ich habe noch kein Beispiel gesehen, in dem eine riesige Klasse, die sich mit einem Dutzend völlig unterschiedlicher Dinge befasst, ein gutes Design ist. Das Einzige, was DI tut, ist, all diese Verstöße offensichtlicher zu machen.
Voo

0

Sie sagen, Sie möchten es verwenden, geben aber nicht an, warum.

DI ist nichts weiter als ein Mechanismus zum Erzeugen von Konkretionen aus Schnittstellen.

Dies an sich ergibt sich aus dem DIP . Wenn Ihr Code bereits in diesem Stil geschrieben ist und Sie eine einzige Stelle haben, an der Konkretisierungen generiert werden, bringt DI der Party nichts mehr. Das Hinzufügen von DI-Framework-Code würde Ihre Codebasis einfach aufblähen und verschleiern.

Angenommen , Sie haben es verwenden möchten, richten Sie Sie in der Regel die Fabrik / builder / Container (oder was auch immer) früh in der Anwendung , so dass es deutlich sichtbar ist.

NB, es ist sehr einfach, eigene zu rollen, wenn Sie möchten, anstatt sich auf Ninject / StructureMap oder was auch immer festzulegen. Wenn Sie jedoch eine angemessene Fluktuation haben, kann dies dazu führen, dass die Räder mit einem anerkannten Rahmen geschmiert werden oder zumindest so geschrieben werden, dass die Lernkurve nicht zu lang wird.


0

Eigentlich ist der "richtige" Weg, KEINE Fabrik zu benutzen, es sei denn, es gibt absolut keine andere Wahl (wie bei Unit-Tests und bestimmten Mocks - für Produktionscode verwenden Sie KEINE Fabrik)! Dies ist eigentlich ein Anti-Pattern und sollte unter allen Umständen vermieden werden. Der springende Punkt hinter einem DI-Container ist, dass das Gadget die Arbeit für Sie erledigt.

Wie bereits in einem früheren Beitrag erwähnt, soll Ihr IoC-Gadget die Verantwortung für die Erstellung der verschiedenen abhängigen Objekte in Ihrer App übernehmen. Das heißt, Sie lassen Ihr DI-Gadget die verschiedenen Instanzen selbst erstellen und verwalten. Dies ist der springende Punkt hinter DI - Ihre Objekte sollten NIEMALS wissen, wie sie die Objekte erstellen und / oder verwalten, von denen sie abhängen. Andernfalls bricht die Kupplung ab.

Das Konvertieren einer vorhandenen Anwendung in alle DIs ist ein großer Schritt. Abgesehen von den offensichtlichen Schwierigkeiten sollten Sie jedoch auch (um Ihr Leben ein wenig zu vereinfachen) ein DI-Tool ausprobieren, das den Großteil Ihrer Bindungen automatisch ausführt (Der Kern von Ninject sind die "kernel.Bind<someInterface>().To<someConcreteClass>()"Aufrufe, die Sie ausführen , um Ihre Schnittstellendeklarationen mit den konkreten Klassen abzugleichen , die Sie zum Implementieren dieser Schnittstellen verwenden möchten. Es sind diese "Bind" -Aufrufe, mit denen Ihr DI-Gadget Ihre Konstruktoraufrufe abfangen und die bereitstellen kann Erforderliche abhängige Objektinstanzen Ein typischer Konstruktor (Pseudocode hier gezeigt) für eine Klasse könnte sein:

public class SomeClass
{
  private ISomeClassA _ClassA;
  private ISomeOtherClassB _ClassB;

  public SomeClass(ISomeClassA aInstanceOfA, ISomeOtherClassB aInstanceOfB)
  {
    if (aInstanceOfA == null)
      throw new NullArgumentException();
    if (aInstanceOfB == null)
      throw new NullArgumentException();
    _ClassA = aInstanceOfA;
    _ClassB = aInstanceOfB;
  }

  public void DoSomething()
  {
    _ClassA.PerformSomeAction();
    _ClassB.PerformSomeOtherActionUsingTheInstanceOfClassA(_ClassA);
  }
}

Beachten Sie, dass sich an keiner Stelle in diesem Code ein Code befand, der die Instanz von SomeConcreteClassA oder SomeOtherConcreteClassB erstellt / verwaltet / freigegeben hat. Tatsächlich wurde keine konkrete Klasse erwähnt. Also ... wo ist die Magie passiert?

Im Start-Teil Ihrer App hat Folgendes stattgefunden (dies ist wiederum Pseudo-Code, aber er kommt dem echten (Ninject-) Ding ziemlich nahe ...):

public void StartUp()
{
  kernel.Bind<ISomeClassA>().To<SomeConcreteClassA>();
  kernel.Bind<ISomeOtherClassB>().To<SomeOtherConcreteClassB>();
}

Dieser kleine Code fordert das Ninject-Gadget auf, nach Konstruktoren zu suchen, diese zu scannen, nach Instanzen von Schnittstellen zu suchen, für die es konfiguriert wurde (das sind die "Bind" -Aufrufe), und dann eine Instanz der konkreten Klasse überall zu erstellen und zu ersetzen Die Instanz wird referenziert.

Es gibt ein nettes Tool, das Ninject sehr gut ergänzt und Ninject.Extensions.Conventions (ein weiteres NuGet-Paket) heißt und den Großteil dieser Arbeit für Sie erledigt. Nicht von den hervorragenden Lernerfahrungen abzulenken, die Sie während des Aufbaus selbst miterleben werden, aber um sich an die Arbeit zu machen, ist dies möglicherweise ein Hilfsmittel, um dies zu untersuchen.

Wenn Speicher zur Verfügung steht, verfügt Unity (offiziell von Microsoft, jetzt ein Open Source-Projekt) über einen oder zwei Methodenaufrufe, die dasselbe tun. Andere Tools verfügen über ähnliche Hilfsprogramme.

Welchen Weg Sie auch wählen, lesen Sie unbedingt Mark Seemanns Buch für den Großteil Ihres DI-Trainings. Es sollte jedoch darauf hingewiesen werden, dass selbst die "Großen" der Welt der Softwaretechnik (wie Mark) grelle Fehler machen können - Mark hat alles vergessen Ninject in seinem Buch. Hier ist eine weitere Ressource, die nur für Ninject geschrieben wurde. Ich habe es und es ist eine gute Lektüre: Mastering Ninject for Dependency Injection


0

Es gibt keinen "richtigen Weg", aber es gibt ein paar einfache Prinzipien, denen man folgen muss:

  • Erstellen Sie das Kompositionsstammverzeichnis beim Start der Anwendung
  • Nachdem das Kompositionsstammverzeichnis erstellt wurde, werfen Sie den Verweis auf den DI-Container / Kernel weg (oder kapseln Sie ihn zumindest ein, damit Sie in Ihrer Anwendung nicht direkt darauf zugreifen können).
  • Keine Instanzen über "neu" erstellen
  • Übergeben Sie alle erforderlichen Abhängigkeiten als Abstraktion an den Konstruktor

Das ist alles. Sicher, das sind Prinzipien, keine Gesetze, aber wenn Sie sie befolgen, können Sie sicher sein, dass Sie DI tun (bitte korrigieren Sie mich, wenn ich falsch liege).


Wie kann man also zur Laufzeit Objekte erstellen, ohne "neu" zu sein und ohne den DI-Container zu kennen?

Im Falle von NInject gibt es eine Factory-Erweiterung , mit der Fabriken erstellt werden können. Sicher, die erstellten Fabriken haben immer noch eine interne Referenz zum Kernel, aber auf diese kann in Ihrer Anwendung nicht zugegriffen werden.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.