2009-12-12 20 views
8

Ziemlich neu in Dependency Injection und ich versuche herauszufinden, ob dies ein Anti-Pattern ist.Den Dependency Injector durch Dependency Injection injizieren

Sagen wir, ich habe 3 Baugruppen:

Foo.Shared - this has all the interfaces 
Foo.Users - references Foo.Shared 
Foo.Payment - references Foo.Shared 

Foo.Users braucht ein Objekt, das innerhalb Foo.Payment gebaut wird, und Foo.Payment muss auch Sachen von Foo.Users. Dies schafft eine Art zirkuläre Abhängigkeit.

Ich habe in Foo.Shared eine Schnittstelle definiert, die das Dependency Injection-Framework, das ich verwende (in diesem Fall NInject), als Proxy verwendet.

public interface IDependencyResolver 
{ 
    T Get<T>(); 
} 

In der Container-Anwendung habe ich eine Implementierung dieser Schnittstelle:

public class DependencyResolver:IDependencyResolver 
{ 
    private readonly IKernel _kernel; 

    public DependencyResolver(IKernel kernel) 
    { 
     _kernel = kernel; 
    } 

    public T Get<T>() 
    { 
     return _kernel.Get<T>(); 
    } 
} 

Die Konfiguration sieht wie folgt aus:

public class MyModule:StandardModule 
{ 
    public override void Load() 
    { 
     Bind<IDependencyResolver>().To<DependencyResolver>().WithArgument("kernel", Kernel); 
     Bind<Foo.Shared.ISomeType>().To<Foo.Payment.SomeType>(); // <- binding to different assembly 
     ... 
    } 
} 

Dies ermöglicht es mir ein neues Objekt der Foo.Payment.SomeType zu instanziiert von innen Foo.Users ohne eine direkte Referenz benötigen:

public class UserAccounts:IUserAccounts 
{ 
    private ISomeType _someType; 
    public UserAccounts(IDependencyResolver dependencyResolver) 
    { 
     _someType = dependencyResolver.Get<ISomeType>(); // <- this essentially creates a new instance of Foo.Payment.SomeType 
    } 
} 

Dies macht es unklar, was die genauen Abhängigkeiten der UserAccounts Klasse in diesem Fall sind, die mich denken, dass es keine gute Praxis ist.

Wie sonst kann ich das erreichen?

Irgendwelche Gedanken?

+1

+1 nur für den Zungenbrecher Titel. – womp

+0

gleich hier, ich liebe den Titel :) –

Antwort

7

Obwohl etwas umstritten: Ja, das ist ein Anti-Muster. Es ist bekannt als Service Locator und während einige es für ein richtiges Design-Muster halten, halte ich es für ein Anti-Pattern.

Dieses Problem ist, dass die Verwendung von z.B. Ihre UserAccounts-Klasse wird implizit statt explizite. Während der Konstruktor angibt, dass er einen IDependencyResolver benötigt, gibt er nicht an, was in ihm enthalten sein soll. Wenn Sie ein IDependencyResolver übergeben, das ISomeType nicht auflösen kann, wird es ausgelöst.

Was ist schlimmer ist, dass bei späteren Iterationen möglicherweise versucht werden, einige andere Typ aus UserAccounts zu lösen. Es wird gut kompilieren, aber wird wahrscheinlich zur Laufzeit werfen, wenn/wenn der Typ nicht aufgelöst werden kann.

Gehen Sie nicht diesen Weg.

Aus den gegebenen Informationen ist es unmöglich, Ihnen genau zu sagen, wie Sie Ihr spezielles Problem mit zirkulären Abhängigkeiten lösen sollten, aber ich würde vorschlagen, dass Sie Ihr Design überdenken. In vielen Fällen sind zirkuläre Referenzen ein Symptom von Leaky Abstractions, also vielleicht, wenn Sie Ihre API ein wenig umgestalten, wird es weggehen - es ist oft überraschend, wie kleine Änderungen erforderlich sind.

Im Allgemeinen besteht die Lösung für ein Problem darin, eine weitere Indirektionsebene hinzuzufügen. Wenn Objekte aus beiden Bibliotheken wirklich eng zusammenarbeiten müssen, können Sie in der Regel einen Zwischenbroker einrichten.

  • In vielen Fällen ist ein Modell funktioniert gut Publish/Subscribe.
  • Die Mediator Muster kann eine Alternative bieten, wenn die Kommunikation in beide Richtungen gehen muss.
  • Sie können auch eine Abstract Factory eingeben, um die Instanz, die Sie benötigen, wie Sie sie benötigen, statt dass sie sofort verdrahtet werden müssen.
+0

Das ist, was ich dachte, die Abhängigkeiten sind nicht klar, so muss es ein Antipattern sein. :) Eine abstrakte Fabrik war meine erste Wahl, aber sie verschleierte den Code. In der realen Anwendung muss ich viele verschiedene Typen erstellen, nicht nur einen. Ich programmiere entweder jede andere Fabrikmethode fest oder verwende Generika, um eine Schnittstelle zu einer konkreten Klasse (auch hardcoded) zuzuordnen. Aber ich verliere die Macht des Dependency-Injection-Frameworks auf diese Weise, und es wird wirklich unordentlich, die Bindungen zu konfigurieren, sogar bis zu dem Punkt, dass man manuelle Abhängigkeitsinjektion/Typ-Resolver-Code durch Reflexion benötigt. – andreialecu

+0

Es gibt immer einen Preis zu zahlen :) Ich stimme nicht zu, dass es unordentlich wird, den Container zu konfigurieren - es kann ziemlich langatmig und ausführlich werden, aber es wird aus einer Menge fast deklarativem Code bestehen, der ein ist ausgezeichneter Kompromiss. Ein großer Teil der Langatmigkeit, die Sie mit der konventionsbasierten Konfiguration lösen können, ist besonders dann gegeben, wenn Sie viele ähnliche abstrakte Fabriken haben, die alle auf die gleiche Weise konfiguriert werden müssen. Castle Windsor verfügt über Funktionen, die es ermöglichen, per Konvention einige einfache Anweisungen zu konfigurieren - ich weiß nicht, ob NInject das auch kann ... –

1

Das scheint mir etwas merkwürdig. Ist es möglich, die Logik, die beide Referenzen benötigt, in eine dritte Assembly zu zerlegen, um die Abhängigkeiten zu brechen und das Risiko zu vermeiden?

2

Ich stimme ForeverDebugging zu - es wäre gut, die zirkuläre Abhängigkeit zu beseitigen. Sehen Sie, wenn Sie die Klassen wie folgt trennen:

  • Foo.Payment.dll: Klassen, die nur mit der Zahlung befassen, nicht mit den Nutzern
  • Foo.Users.dll: Klassen, die nur mit Benutzern beschäftigen, nicht mit Zahlung
  • Foo.UserPayment.dll: Klassen, die sowohl mit Zahlung und Anwender beschäftigen

Dann Sie eine Baugruppe, die zwei andere verweisen, aber keinen Kreis von Abhängigkeiten.

Wenn Sie eine zirkuläre Abhängigkeit zwischen Assemblys haben, bedeutet dies nicht unbedingt, dass Sie eine zirkuläre Abhängigkeit zwischen den Klassen haben. Angenommen, Sie diese Abhängigkeiten haben:

  • Foo.Users.UserAccounts hängt von Foo.Shared.IPaymentHistory, die von Foo.Payment.PaymentHistory umgesetzt wird.
  • Eine andere Zahlungsklasse, Foo.Payment.PaymentGateway, hängt von Foo.Shared.IUserAccounts ab. IUserAccounts wird von Foo.Users.UserAccounts implementiert.

Angenommen, es gibt keine anderen Abhängigkeiten.

Hier gibt es einen Kreis von Assemblys, die zur Laufzeit in Ihrer Anwendung voneinander abhängig sind (obwohl sie zur Kompilierzeit nicht aufeinander angewiesen sind, da sie die freigegebene DLL durchlaufen). Aber es gibt keinen Kreis von Klassen, die voneinander abhängen, zur Kompilierungszeit oder zur Laufzeit.

In diesem Fall sollten Sie Ihren IoC-Container weiterhin normal verwenden können, ohne eine zusätzliche Indirektionsebene hinzuzufügen. In Ihrem MyModule binden Sie einfach jede Schnittstelle an den entsprechenden konkreten Typ. Lassen Sie jede Klasse ihre Abhängigkeiten als Argumente für den Konstruktor akzeptieren. Wenn Ihr Top-Level-Anwendungscode eine Instanz einer Klasse benötigt, fragen Sie den IoC-Container nach der Klasse. Lassen Sie den IoC-Container sich darum kümmern, alles zu finden, worauf die Klasse angewiesen ist.



Wenn Sie mit einer zirkulären Abhängigkeit zwischen den Klassen am Ende tun, müssen Sie wahrscheinlich Eigenschaft Injektion (aka Setter Injektion) auf einem der Klassen verwenden, statt Konstruktor Injektion. Ich benutze Ninject nicht, aber es unterstützt Eigenschaft Injektion - here is the documentation.

Normalerweise verwenden IoC-Container die Konstruktorinjektion - sie übergeben Abhängigkeiten an den Konstruktor der Klasse, die von ihnen abhängt. Dies funktioniert jedoch nicht, wenn eine zirkuläre Abhängigkeit besteht. Wenn die Klassen A und B voneinander abhängig sind, müssen Sie eine Instanz der Klasse A in dem Konstruktor der Klasse B übergeben. Um jedoch ein A zu erstellen, müssen Sie eine Instanz der Klasse B in den Konstruktor übergeben. Es ist ein Huhn-und-Ei-Problem.

Mit der Eigenschaft injection weisen Sie Ihren IoC-Container an, zuerst den Konstruktor aufzurufen und dann eine Eigenschaft für das konstruierte Objekt festzulegen. Normalerweise wird dies für optionale Abhängigkeiten wie Logger verwendet. Sie können es aber auch verwenden, um eine zirkuläre Abhängigkeit zwischen zwei Klassen zu unterbrechen, die einander erfordern.

Dies ist jedoch nicht schön, und ich würde definitiv empfehlen, Ihre Klassen umzuformen, um zirkuläre Abhängigkeiten zu beseitigen.