web-dev-qa-db-fra.com

Est-il préférable de créer un singleton pour accéder au conteneur de l'unité ou le transmettre via l'application?

Je me permets d'utiliser un framework IoC et j'ai choisi d'utiliser Unity. Une des choses que je ne comprends toujours pas à fond est comment résoudre les objets plus en profondeur dans l'application. Je suppose que je n'ai tout simplement pas eu l'ampoule au moment qui le rend clair.

J'essaie donc de faire quelque chose comme ce qui suit dans le code psuedo'ish

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

Donc, le testSuiteParser.Parse fait ce qui suit

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);
    } 
}

Je peux voir trois options:

  1. Créez un Singleton pour stocker mon conteneur d'unité auquel je peux accéder n'importe où. Je ne suis vraiment pas fan de cette approche. Ajouter une dépendance comme celle d'utiliser un framework d'injection de dépendance semble un peu étrange.
  2. Transmettez IUnityContainer à ma classe TestSuiteParser et à chaque enfant de celui-ci (supposons qu’il s’agisse de n niveaux de profondeur ou en réalité d’environ 3 niveaux de profondeur). Passer partout dans IUnityContainer semble étrange. J'ai peut-être juste besoin de surmonter cela.
  3. Avoir le moment ampoule sur la bonne façon d'utiliser Unity. En espérant que quelqu'un puisse aider à actionner l'interrupteur.

[EDIT] Une des choses sur laquelle je n’étais pas clair, c’est que je veux créer une nouvelle instance de test case pour chaque itération de la déclaration foreach. L'exemple ci-dessus doit analyser une configuration de suite de tests et remplir une collection d'objets de scénario de test.

48
btlog

L'approche correcte pour DI consiste à utiliser Injection de constructeur ou un autre motif de DI (mais l'injection de constructeur est la plus courante) pour injecter les dépendances dans le consommateur, indépendamment de DI Container .

Dans votre exemple, il semble que vous ayez besoin des dépendances TestSuite et TestCase. Votre classe TestSuiteParser doit donc annoncer de manière statique qu'elle a besoin de ces dépendances en les demandant via son constructeur (unique):

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;
    }

    // ...
}

Notez que la combinaison du mot clé readonly et de la clause de garde protège les invariants de la classe, en veillant à ce que les dépendances will soient disponibles pour toute instance de TestSuiteParser créée avec succès.

Vous pouvez maintenant implémenter la méthode Parse comme ceci:

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

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

(Cependant, je soupçonne qu’il peut y avoir plus d’un TestCase impliqué, auquel cas vous voudrez peut-être injecter un Abstract Factory au lieu d’un seul TestCase.)

À partir de votre Composition Root , vous pouvez configurer Unity (ou tout autre conteneur):

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

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

Lorsque le conteneur résout TestSuiteParser, il comprend le modèle d'injection de constructeur, donc il Auto-Wires l'instance avec toutes ses dépendances requises.

La création d'un conteneur Singleton ou le transfert du conteneur ne sont que deux variantes de l'anti-pattern Service Locator , je ne le recommanderais donc pas.

51
Mark Seemann

Je suis nouveau à Dependency Injection et j'avais aussi cette question. J'avais du mal à penser à DI, principalement parce que je me concentrais sur l'application de DI à la classe sur laquelle je travaillais et, une fois que j'ai ajouté les dépendances au constructeur, j'ai immédiatement essayé de trouver un moyen d'obtenir l'unité. conteneur aux endroits où cette classe devait être instanciée afin que je puisse appeler la méthode Resolve sur la classe. En conséquence, je réfléchissais à la possibilité de rendre le conteneur unit disponible globalement sous forme statique ou de l’envelopper dans une classe singleton.

J'ai lu les réponses ici et je n'ai pas vraiment compris ce qui était expliqué. Ce qui m'a finalement aidé à "comprendre", c'est cet article:

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

Et ce paragraphe en particulier était le moment "ampoule":

"99% de votre base de code ne devrait pas connaître votre conteneur IoC. Seule la classe racine ou le programme d'amorçage utilise le conteneur. Même dans ce cas, un seul appel de résolution suffit généralement à créer votre graphe de dépendance. et lancez l'application ou la demande. "

Cet article m'a aidé à comprendre que je ne dois pas accéder au conteneur d'unité dans l'application, mais uniquement à la racine de celle-ci. Je dois donc appliquer le principe de DI à plusieurs reprises jusqu'à la classe racine de l'application.

J'espère que cela aide d'autres personnes aussi confuses que moi! :)

12
BruceHill

Vous ne devriez pas vraiment avoir besoin d'utiliser votre conteneur directement dans de très nombreux endroits de votre application. Vous devriez prendre toutes vos dépendances dans le constructeur et ne pas les atteindre avec vos méthodes. Votre exemple pourrait être quelque chose comme ceci:

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);
        } 
    }
}

Et ensuite, vous le faites de la même manière dans l’application. Vous devrez bien sûr, à un moment donné, résoudre quelque chose. Dans asp.net mvc par exemple, cet endroit est dans l’usine du contrôleur. C'est l'usine qui crée le contrôleur. Dans cette usine, vous utiliserez votre conteneur pour résoudre les paramètres de votre contrôleur. Mais ceci n’est qu’un seul endroit dans l’ensemble de l’application (probablement plus lorsque vous faites des choses plus avancées).

Il existe également un projet Nice appelé CommonServiceLocator . Il s'agit d'un projet qui possède une interface partagée pour tous les conteneurs ioc courants, de sorte que vous ne dépendez pas d'un conteneur spécifique.

4

Si seulement un seul pouvait avoir un "ServiceLocator" qui est transmis aux constructeurs de services, mais parvient malgré tout à "Déclarer" les dépendances prévues de la classe dans laquelle il est injecté (c'est-à-dire ne pas masquer les dépendances) ... de cette façon, tout (? ) les objections au modèle de localisateur de service peuvent être levées.

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

Malheureusement, ce qui précède n’existera évidemment que dans mes rêves, car c # interdit les paramètres génériques variables (immobiles). Il serait donc difficile d’ajouter manuellement une nouvelle interface générique chaque fois que l’on aura besoin d’un paramètre générique supplémentaire.

Si par contre, ce qui précède pourrait être atteint malgré la limitation de c # de la manière suivante ...

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

De cette façon, il suffit de faire un dactylographie supplémentaire pour obtenir la même chose. Ce dont je ne suis pas encore sûr, c’est si, étant donné la conception appropriée de la classe TArg (je suppose qu’un ingénieux héritage sera utilisé pour permettre imbrication infinie de TArg paramètres génériques), les conteneurs DI seront en mesure de résoudre la IServiceResolver correctement. L'idée, en fin de compte, est simplement de contourner la même implémentation de la variable IServiceResolver, quelle que soit la déclaration générique trouvée dans le constructeur de la classe dans laquelle on injecte.

0
Dantte