web-dev-qa-db-ger.com

Ist es besser, einen Singleton zu erstellen, um auf den Unity-Container zuzugreifen, oder ihn durch die Anwendung zu leiten?

Ich tauche gerade in ein IoC-Framework ein und habe mich für Unity entschieden. Eines der Dinge, die ich immer noch nicht vollständig verstehe, ist, wie man Objekte tiefer in die Anwendung auflöst. Ich vermute, ich hatte gerade nicht die Glühbirne im Moment, die es klar machen wird.

Also versuche ich etwas wie das Folgende im pseudo-englischen Code zu machen

void Workflow(IUnityContatiner contatiner, XPathNavigator someXml)
{
   testSuiteParser = container.Resolve<ITestSuiteParser>
   TestSuite testSuite = testSuiteParser.Parse(SomeXml) 
   // Do some mind blowing stuff here
}

Das testSuiteParser.Parse führt also Folgendes aus

TestSuite Parse(XPathNavigator someXml)
{
    TestStuite testSuite = ??? // I want to get this from my Unity Container
    List<XPathNavigator> aListOfNodes = DoSomeThingToGetNodes(someXml)

    foreach (XPathNavigator blah in aListOfNodes)
    {
        //EDIT I want to get this from my Unity Container
        TestCase testCase = new TestCase() 
        testSuite.TestCase.Add(testCase);
    } 
}

Ich sehe drei Möglichkeiten:

  1. Erstellen Sie ein Singleton, um meinen Einheitscontainer zu speichern, auf den ich überall zugreifen kann. Ich bin wirklich kein Fan dieses Ansatzes. Das Hinzufügen einer solchen Abhängigkeit zur Verwendung eines Frameworks für die Abhängigkeitsinjektion scheint etwas seltsam.
  2. Übergeben Sie den IUnityContainer an meine TestSuiteParser-Klasse und an jedes untergeordnete Element davon (nehmen Sie an, dass er n Ebenen tief ist oder in Wirklichkeit etwa 3 Ebenen tief ist). IUnityContainer überall herumzugeben, sieht einfach seltsam aus. Ich muss vielleicht nur darüber hinwegkommen.
  3. Bringen Sie die Glühbirne auf den richtigen Weg, um Unity zu verwenden. Ich hoffe, jemand kann helfen, den Schalter zu betätigen.

[EDIT] Eines der Dinge, bei denen ich nicht klar war, ist, dass ich für jede Iteration der foreach-Anweisung eine neue Instanz eines Testfalls erstellen möchte. Im obigen Beispiel muss eine Testsuite-Konfiguration analysiert und eine Sammlung von Testfallobjekten aufgefüllt werden

48
btlog

Der richtige Ansatz für DI ist die Verwendung von Constructor Injection oder eines anderen DI-Musters (wobei Constructor Injection jedoch am häufigsten vorkommt), um die Abhängigkeiten in den Consumer einzuspeisen, unabhängig von DI Container .

In Ihrem Beispiel sieht es so aus, als ob Sie die Abhängigkeiten TestSuite und TestCase benötigen, daher sollte Ihre TestSuiteParser-Klasse statically announce angeben, dass sie diese Abhängigkeiten benötigt, indem Sie sie über ihren (einzigen) Konstruktor abfragen:

public class TestSuiteParser
{
    private readonly TestSuite testSuite;
    private readonly TestCase testCase;

    public TestSuiteParser(TestSuite testSuite, TestCase testCase)
    {
        if(testSuite == null)
        {
            throw new ArgumentNullException(testSuite);
        }
        if(testCase == null)
        {
            throw new ArgumentNullException(testCase);
        }

        this.testSuite = testSuite;
        this.testCase = testCase;
    }

    // ...
}

Beachten Sie, wie die Kombination aus dem Schlüsselwort readonly und der Guard-Klausel die Invarianten der Klasse schützt, um sicherzustellen, dass die Abhängigkeiten will für jede erfolgreich erstellte Instanz von TestSuiteParser verfügbar sind.

Sie können nun die Parse-Methode folgendermaßen implementieren:

public TestSuite Parse(XPathNavigator someXml) 
{ 
    List<XPathNavigator> aListOfNodes = DoSomeThingToGetNodes(someXml) 

    foreach (XPathNavigator blah in aListOfNodes) 
    { 
        this.testSuite.TestCase.Add(this.testCase); 
    }  
} 

(Ich vermute jedoch, dass möglicherweise mehr als ein Testfall beteiligt ist. In diesem Fall möchten Sie vielleicht eine abstrakte Fabrik anstelle eines einzelnen Testfalls einfügen).

In Ihrem Composition Root ) können Sie Unity (oder einen beliebigen anderen Container) konfigurieren:

container.RegisterType<TestSuite, ConcreteTestSuite>();
container.RegisterType<TestCase, ConcreteTestCase>();
container.RegisterType<TestSuiteParser>();

var parser = container.Resolve<TestSuiteParser>();

Wenn der Container TestSuiteParser auflöst, versteht er das Konstruktionsinjektionsmuster, sodass er Auto-Wires die Instanz mit allen erforderlichen Abhängigkeiten darstellt.

Das Erstellen eines Singleton-Containers oder das Weitergeben des Containers sind nur zwei Variationen des Service Locator Anti-Pattern , daher würde ich das nicht empfehlen.

51
Mark Seemann

Ich bin neu bei Dependency Injection und hatte auch diese Frage. Ich hatte Schwierigkeiten, mich mit DI zu beschäftigen, hauptsächlich, weil ich mich darauf konzentrierte, DI nur auf die eine Klasse anzuwenden, an der ich gerade arbeitete. Sobald ich die Abhängigkeiten zum Konstruktor hinzugefügt hatte, versuchte ich sofort, einen Weg zu finden, um die Einheit zu erreichen Container zu den Stellen, an denen diese Klasse instanziiert werden musste, damit ich die Resolve-Methode für die Klasse aufrufen kann. Daher dachte ich daran, den Unity-Container global als statisch verfügbar zu machen oder in eine Singleton-Klasse einzuwickeln.

Ich habe die Antworten hier gelesen und nicht wirklich verstanden, was erklärt wurde. Was mir schließlich "geholfen" hat, war dieser Artikel:

http://www.devtrends.co.uk/blog/how-not-to-do-dependency-injection-the-static-or-singleton-container

Und dieser Absatz war insbesondere der Moment der "Glühbirne":

"99% Ihrer Codebasis sollten über Ihren IoC-Container keine Kenntnis haben. Es ist nur die Stammklasse oder der Bootstrapper, die den Container verwendet. Selbst dann ist ein einzelner Auflösungsaufruf alles, was normalerweise zum Erstellen Ihres Abhängigkeitsgraphen erforderlich ist und starten Sie die Anwendung oder Anfrage. "

Dieser Artikel hat mir geholfen zu verstehen, dass ich eigentlich nicht in der gesamten Anwendung auf den Unity-Container zugreifen darf, sondern nur im Stammverzeichnis der Anwendung. Also muss ich das DI-Prinzip immer wieder auf die Wurzelklasse der Anwendung anwenden.

Ich hoffe, das hilft anderen, die genauso verwirrt sind wie ich! :)

12
BruceHill

Sie sollten Ihren Container nicht direkt an sehr vielen Stellen Ihrer Anwendung verwenden müssen. Sie sollten alle Ihre Abhängigkeiten im Konstruktor übernehmen und nicht über Ihre Methoden erreichen. Ihr Beispiel könnte so aussehen:

public class TestSuiteParser : ITestSuiteParser {
    private TestSuite testSuite;

    public TestSuitParser(TestSuit testSuite) {
        this.testSuite = testSuite;
    }

    TestSuite Parse(XPathNavigator someXml)
    {
        List<XPathNavigator> aListOfNodes = DoSomeThingToGetNodes(someXml)

        foreach (XPathNavigator blah in aListOfNodes)
        {
            //I don't understand what you are trying to do here?
            TestCase testCase = ??? // I want to get this from my Unity Container
            testSuite.TestCase.Add(testCase);
        } 
    }
}

Und dann machen Sie es auf die gleiche Weise in der gesamten Anwendung. Sie werden natürlich irgendwann etwas lösen müssen. In asp.net mvc befindet sich diese Stelle beispielsweise in der Steuerungsfabrik. Das ist die Fabrik, die den Controller erstellt. In dieser Fabrik verwenden Sie Ihren Container, um die Parameter für Ihren Controller aufzulösen. Dies ist jedoch nur eine Stelle in der gesamten Anwendung (wahrscheinlich einige andere Stellen, wenn Sie fortgeschrittenere Dinge tun).

Es gibt auch ein Nizza-Projekt namens CommonServiceLocator . Dies ist ein Projekt, das über eine gemeinsame Schnittstelle für alle gängigen Ioc-Container verfügt, sodass Sie nicht von einem bestimmten Container abhängig sind.

4

Wenn nur einer über einen "ServiceLocator" verfügen könnte, der um Service-Konstruktoren herumgereicht wird, es aber irgendwie gelingt, die beabsichtigten Abhängigkeiten der Klasse, in die er eingefügt wird, zu "deklarieren" (dh die Abhängigkeiten nicht zu verbergen) ... auf diese Weise alle (? ) Einwände gegen das Service Locator-Muster können aufgehoben werden.

public class MyBusinessClass
{
    public MyBusinessClass(IServiceResolver<Dependency1, Dependency2, Dependency3> locator)
    {
        //keep the resolver for later use
    }
}

Leider existiert das Obige natürlich nur in meinen Träumen, da c # variable generische Parameter (noch) verbietet. Das manuelle Hinzufügen eines neuen generischen Interfaces jedes Mal, wenn ein zusätzlicher generischer Parameter benötigt wird, wäre umständlich.

Wenn andererseits das Obige trotz der Einschränkung von c # auf folgende Weise erreicht werden könnte ...

public class MyBusinessClass
{
    public MyBusinessClass(IServiceResolver<TArg<Dependency1, TArg<Dependency2, TArg<Dependency3>>> locator)
    {
        //keep the resolver for later use
    }
}

Auf diese Weise muss man nur noch extra tippen, um dasselbe zu erreichen. Was ich noch nicht sicher bin, ist, wenn man das richtige Design der TArg-Klasse voraussetzt (ich gehe davon aus, dass eine kluge Vererbung verwendet wird, um dies zuzulassen.) unendliche Verschachtelung von TArg generischen Parametern), DI-Container können die IServiceResolver richtig auflösen. Die Idee ist letztlich, einfach dieselbe Implementierung der IServiceResolver zu übergeben, unabhängig von der generischen Deklaration im Konstruktor der Klasse, in die eingefügt wird.

0
Dantte