Ioc / DI - Warum muss ich alle Ebenen / Baugruppen im Einstiegspunkt der Anwendung referenzieren?

123

(Im Zusammenhang mit dieser Frage, EF4: Warum muss die Proxy-Erstellung aktiviert werden, wenn das verzögerte Laden aktiviert ist? )

Ich bin neu bei DI, also nimm es mit. Ich verstehe, dass der Container für die Instanziierung aller meiner registrierten Typen zuständig ist. Dazu ist jedoch ein Verweis auf alle DLLs in meiner Lösung und deren Verweise erforderlich.

Wenn ich keinen DI-Container verwenden würde, müsste ich in meiner MVC3-App nicht auf die EntityFramework-Bibliothek verweisen, sondern nur auf meine Business-Schicht, die auf meine DAL / Repo-Schicht verweist.

Ich weiß, dass am Ende des Tages alle DLLs im bin-Ordner enthalten sind, aber mein Problem besteht darin, dass ich explizit über "Verweis hinzufügen" in VS darauf verweisen muss, um einen WAP mit allen erforderlichen Dateien veröffentlichen zu können.

diegohb
quelle
1
Dieser Auszug aus dem Buch Dependency Injection in .NET, zweite Ausgabe, ist eine ausführlichere Version der Antworten von Mark und mir. Es beschreibt ausführlich das Konzept des Composition Root und warum es eigentlich eine gute Sache ist, den Startpfad der Anwendung von jedem anderen Modul abhängig zu machen.
Steven
Ich habe diesen Auszug und Kapitel 1 gelesen und werde das Buch kaufen, da mir die Analogien und einfachen Erklärungen zur komplexen Angelegenheit von DI sehr gut gefallen haben. Ich denke, Sie sollten eine neue Antwort vorschlagen, klar antworten: "Sie müssen nicht alle Ebenen / Baugruppen in der logischen Eingabeebene referenzieren, es sei denn, es ist auch Ihre Kompositionswurzel", den Link zum Auszug und das Bild Abbildung 3 aus dem Auszug.
Diegobb

Antworten:

194

Wenn ich keinen DI-Container verwenden würde, müsste ich in meiner MVC3-App nicht auf die EntityFramework-Bibliothek verweisen, sondern nur auf meine Business-Schicht, die auf meine DAL / Repo-Schicht verweist.

Ja, genau das ist die Situation, die DI so schwer zu vermeiden ist :)

Bei eng gekoppeltem Code hat jede Bibliothek möglicherweise nur wenige Referenzen, aber auch diese haben andere Referenzen, wodurch ein tiefes Diagramm von Abhängigkeiten wie folgt erstellt wird:

Deep Graph

Da die Abhängigkeitsgraphen tief ist, bedeutet dies, dass die meisten Bibliotheken entlang viele andere Abhängigkeiten ziehen - zum Beispiel in dem Diagramm - Bibliothek C schleppt Bibliothek H, Bibliothek E, Bibliothek J, Bibliothek M, Bibliothek K und Bibliothek N . Dies macht es schwieriger, jede Bibliothek unabhängig von den anderen wiederzuverwenden - beispielsweise beim Testen von Einheiten .

In einer lose gekoppelten Anwendung wird der Abhängigkeitsgraph jedoch stark verschoben , indem alle Verweise auf die Kompositionswurzel verschoben werden :

Flaches Diagramm

Wie die grüne Farbe zeigt, ist es jetzt möglich, Bibliothek C wiederzuverwenden, ohne unerwünschte Abhängigkeiten mit sich zu ziehen.

Doch alles , was gesagt, mit vielen DI Container, die Sie nicht haben , um harte Verweise auf alle benötigten Bibliotheken hinzufügen. Stattdessen können Sie die späte Bindung entweder in Form eines konventionellen Assembly-Scans (bevorzugt) oder einer XML-Konfiguration verwenden.

Wenn Sie dies tun, müssen Sie jedoch daran denken, die Assemblys in den Bin-Ordner der Anwendung zu kopieren, da dies nicht mehr automatisch geschieht. Persönlich finde ich diese zusätzliche Anstrengung selten wert.

Eine ausführlichere Version dieser Antwort finden Sie in diesem Auszug aus meinem Buch Dependency Injection, Principles, Practices, Patterns .

Mark Seemann
quelle
3
Vielen Dank, das macht jetzt vollkommen Sinn. Ich musste wissen, ob dies beabsichtigt war. Um die korrekte Verwendung von Abhängigkeiten durchzusetzen, hatte ich mit meinem DI-Bootstrapper wie Steven, der unten erwähnt wird, ein separates Projekt implementiert, in dem ich auf den Rest der Bibliotheken verweise. Dieses Projekt wird von der Einstiegspunkt-App referenziert. Am Ende des vollständigen Builds befinden sich alle erforderlichen DLLs im Ordner bin. Vielen Dank!
Diegohb
2
@ Mark Seemann Ist diese Frage / Antwort spezifisch für Microsoft? Ich würde gerne wissen, ob diese Idee, alle Abhängigkeiten an den "Einstiegspunkt der Anwendung" zu verschieben, für ein Java EE / Spring-Projekt mit Maven sinnvoll ist… danke!
Grégoire C
5
Diese Antwort gilt über .NET hinaus. Vielleicht möchten Sie sich auf das Kapitel Prinzipien des Paketdesigns von Robert C. Martin beziehen, z. B. Agile Softwareentwicklung, Prinzipien, Muster und Praktiken
Mark Seemann,
7
@AndyDangerGagne Die Kompositionswurzel ist ein DI-Muster - das Gegenteil von Service Locator . Aus der Sicht der Kompositionswurzel ist keiner der Typen polymorph; Die Kompositionswurzel betrachtet alle Typen als konkrete Typen, und daher gilt das Liskov-Substitutionsprinzip nicht für sie.
Mark Seemann
4
In der Regel sollten Schnittstellen von den Clients definiert werden, die sie verwenden ( APP, Kapitel 11 ). Wenn also Bibliothek J eine Schnittstelle benötigt, sollte diese in Bibliothek J definiert werden. Dies ist eine Folge des Abhängigkeitsinversionsprinzips.
Mark Seemann
65

Wenn ich keinen DI-Container verwenden würde, müsste ich in meiner MVC3-App nicht auf die EntityFramework-Bibliothek verweisen

Selbst wenn Sie einen DI-Container verwenden, müssen Sie Ihr MVC3-Projekt nicht auf EF verweisen lassen, aber Sie entscheiden sich (implizit) dafür, indem Sie den Kompositionsstamm (den Startpfad, in dem Sie Ihre Objektdiagramme erstellen) in Ihrem MVC3-Projekt implementieren. Wenn Sie Ihre Architekturgrenzen mithilfe von Assemblys sehr streng schützen möchten, können Sie Ihre Präsentationslogik entweder in ein anderes Projekt verschieben.

Wenn Sie die gesamte MVC-bezogene Logik (Controller usw.) aus dem Startprojekt in eine Klassenbibliothek verschieben, bleibt diese Präsentationsschicht-Assembly vom Rest der Anwendung getrennt. Ihr Webanwendungsprojekt selbst wird zu einer sehr dünnen Hülle mit der erforderlichen Startlogik. Das Webanwendungsprojekt ist das Composition Root, das auf alle anderen Assemblys verweist.

Das Extrahieren der Präsentationslogik in eine Klassenbibliothek kann die Arbeit mit MVC erschweren. Es wird schwieriger sein, alles zu verkabeln, da sich die Controller nicht im Startprojekt befinden (während Ansichten, Bilder und CSS-Dateien wahrscheinlich im Startprojekt verbleiben müssen). Dies ist wahrscheinlich machbar, die Einrichtung dauert jedoch länger.

Aufgrund der Nachteile empfehle ich generell, nur die Kompositionswurzel im Webprojekt zu belassen. Viele Entwickler möchten nicht, dass ihre MVC-Assembly von der DAL-Assembly abhängt, aber das ist kein wirkliches Problem. Vergessen Sie nicht, dass Assemblys ein Bereitstellungsartefakt sind . Sie teilen Code in mehrere Assemblys auf, damit Code separat bereitgestellt werden kann. Eine architektonische Schicht ist dagegen ein logisches Artefakt. Es ist sehr gut möglich (und üblich), mehrere Schichten in derselben Baugruppe zu haben.

In diesem Fall befinden sich der Composition Root (Layer) und der Presentation Layer im selben Webanwendungsprojekt (also in derselben Assembly). Und obwohl diese Versammlung verweist auf die Baugruppe die DAL enthält, das Presentation - Layer noch verweist nicht auf die Datenzugriffsschicht . Dies ist ein großer Unterschied.

Wenn wir dies tun, verlieren wir natürlich die Fähigkeit des Compilers, diese Architekturregel beim Kompilieren zu überprüfen, aber dies sollte kein Problem sein. Die meisten Architekturregeln können vom Compiler nicht überprüft werden, und es gibt immer so etwas wie gesunden Menschenverstand. Und wenn es in Ihrem Team keinen gesunden Menschenverstand gibt, können Sie immer Codeüberprüfungen verwenden (was IMO übrigens jedes Team immer tun sollte). Sie können auch ein Tool wie NDepend (kommerziell) verwenden, mit dem Sie Ihre Architekturregeln überprüfen können. Wenn Sie NDepend in Ihren Erstellungsprozess integrieren, kann es Sie warnen, wenn jemand Code eingecheckt hat, der gegen eine solche Architekturregel verstößt.

Eine ausführlichere Diskussion über die Funktionsweise der Kompositionswurzel finden Sie in Kapitel 4 meines Buches Abhängigkeitsinjektion, Prinzipien, Praktiken, Muster .

Steven
quelle
Ein separates Projekt für das Bootstrapping war meine Lösung, da wir kein Ndepend haben und ich es noch nie zuvor verwendet habe. Ich werde es jedoch untersuchen, da es sich nach einem besseren Weg anhört, um das zu erreichen, was ich versuche, wenn es nur eine Endanwendung gibt.
Diegohb
1
Der letzte Absatz ist großartig und hilft mir, meine Meinung darüber zu ändern, wie streng ich bin, wenn ich Ebenen in getrennten Baugruppen halte. Zwei oder mehr logische Ebenen in einer Assembly zu haben, ist in Ordnung, wenn Sie andere Prozesse zum Schreiben von Code verwenden (z. B. Codeüberprüfungen), um sicherzustellen, dass in Ihrem UI-Code keine Verweise auf DAL-Klassen vorhanden sind, und umgekehrt.
BenM
6

Wenn ich keinen DI-Container verwenden würde, müsste ich in meiner MVC3-App nicht auf die EntityFramework-Bibliothek verweisen, sondern nur auf meine Business-Schicht, die auf meine DAL / Repo-Schicht verweist.

Sie können ein separates Projekt mit dem Namen "DependencyResolver" erstellen. In diesem Projekt müssen Sie alle Ihre Bibliotheken referenzieren.

Jetzt benötigt die UI-Ebene kein NHibernate / EF oder eine andere nicht UI-relevante Bibliothek außer Castle Windsor, auf die verwiesen werden soll.

Wenn Sie Castle Windsor und DependencyResolver von Ihrer UI-Ebene ausblenden möchten, können Sie ein HttpModule schreiben, das das IoC-Registrierungsmaterial aufruft.

Ich habe nur ein Beispiel für StructureMap:

public class DependencyRegistrarModule : IHttpModule
{
    private static bool _dependenciesRegistered;
    private static readonly object Lock = new object();

    public void Init(HttpApplication context)
    {
        context.BeginRequest += (sender, args) => EnsureDependenciesRegistered();
    }

    public void Dispose() { }

    private static void EnsureDependenciesRegistered()
    {
        if (!_dependenciesRegistered)
        {
            lock (Lock)
            {
                if (!_dependenciesRegistered)
                {
                    ObjectFactory.ResetDefaults();

                    // Register all you dependencies here
                    ObjectFactory.Initialize(x => x.AddRegistry(new DependencyRegistry()));

                    new InitiailizeDefaultFactories().Configure();
                    _dependenciesRegistered = true;
                }
            }
        }
    }
}

public class InitiailizeDefaultFactories
{
    public void Configure()
    {
        StructureMapControllerFactory.GetController = type => ObjectFactory.GetInstance(type);
          ...
    }
 }

Die DefaultControllerFactory verwendet den IoC-Container nicht direkt, sondern delegiert an IoC-Containermethoden.

public class StructureMapControllerFactory : DefaultControllerFactory
{
    public static Func<Type, object> GetController = type =>
    {
        throw new  InvalidOperationException("The dependency callback for the StructureMapControllerFactory is not configured!");
    };

    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
        if (controllerType == null)
        {
            return base.GetControllerInstance(requestContext, controllerType);
        }
        return GetController(controllerType) as Controller;
    }
}

Der GetControllerDelegat wird in einer StructureMap-Registrierung festgelegt (in Windsor sollte es sich um ein Installationsprogramm handeln).

Rookian
quelle
1
Ich mag das noch besser als das, was ich letztendlich gemacht habe. Module sind großartig. Wo würde ich dann Container.Dispose () aufrufen? ApplicationEnd- oder EndRequest-Ereignis innerhalb des Moduls ...?
Diegohb
1
@Steven Da sich Global.asax in Ihrer MVC-UI-Ebene befindet. Das HttpModule befindet sich im DependencyResolver-Projekt.
Rookian
1
Der kleine Vorteil ist, dass niemand den IoC-Container in der Benutzeroberfläche verwenden kann. Das heißt, niemand kann den IoC-Container als Service-Locator in der Benutzeroberfläche verwenden.
Rookian
1
Außerdem können Entwickler nicht versehentlich den DAL-Code in der Benutzeroberflächenebene verwenden, da in der Benutzeroberfläche kein fester Verweis auf die Assembly vorhanden ist.
Diegohb
1
Ich habe herausgefunden, wie man dasselbe mit der generischen Registrierungs-API von Bootstrapper macht. Mein UI-Projekt verweist auf Bootstrapper, das Projekt zur Auflösung von Abhängigkeiten, in dem ich meine Registrierungen verkabele, und Projekte in meinem Core (für die Schnittstellen), aber nichts anderes, nicht einmal mein DI Framework (SimpleInjector). Ich verwende OutputTo Nuget, um DLLs in den Ordner bin zu kopieren.
Diegohb
0
  • Es gibt eine Abhängigkeit: Wenn ein Objekt ein anderes Objekt instanziiert.
  • Es gibt keine Abhängigkeit: Wenn ein Objekt eine Abstraktion erwartet (Konstruktorinjektion, Methodeninjektion ...)
  • Assembly-Referenzen (Referenzieren von DLL, Webservices ..) sind unabhängig vom Abhängigkeitskonzept, da die Ebene darauf verweisen muss, um eine Abstraktion aufzulösen und den Code kompilieren zu können.
Riadh Gomri
quelle