49 votes

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

Je trempe mon orteil dans l'utilisation d'un framework IoC et j'ai choisi d'utiliser Unity. Une des choses que je ne comprends pas encore complètement est comment résoudre des objets plus profondément dans l'application. Je soupçonne simplement de ne pas avoir eu le déclic qui rendra cela clair.

Donc, j'essaie de faire quelque chose comme ce qui suit en code pseudo

void Workflow(IUnityContatiner contatiner, XPathNavigator someXml)
{
   testSuiteParser = container.Resolve
   TestSuite testSuite = testSuiteParser.Parse(SomeXml) 
   // Faire des choses incroyables ici
}

Donc, le testSuiteParser.Parse fait ce qui suit

TestSuite Parse(XPathNavigator someXml)
{
    TestStuite testSuite = ??? // Je veux obtenir cela de mon conteneur Unity
    List aListOfNodes = DoSomeThingToGetNodes(someXml)

    foreach (XPathNavigator blah in aListOfNodes)
    {
        //EDIT Je veux obtenir cela de mon conteneur Unity
        TestCase testCase = new TestCase() 
        testSuite.TestCase.Add(testCase);
    } 
}

Je vois trois options:

  1. Créer un Singleton pour stocker mon conteneur Unity auquel je peux accéder n'importe où. Je ne suis vraiment pas fan de cette approche. Ajouter une dépendance comme celle-ci pour utiliser un framework d'injection de dépendances semble un peu bizarre.
  2. Passer l'IUnityContainer à ma classe TestSuiteParser et à chaque enfant de celle-ci (supposez qu'elle soit n niveaux profonde ou en réalité d'environ 3 niveaux de profondeur). Passer l'IUnityContainer partout semble juste bizarre. Je dois peut-être m'en remettre à cela.
  3. Avoir le déclic sur la bonne façon d'utiliser Unity. En espérant que quelqu'un pourra m'aider à allumer l'interrupteur.

[EDIT] Une des choses sur lesquelles je n'étais pas clair 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 du test suite et peupler une collection d'objets de test case

51voto

Mark Seemann Points 102767

La bonne approche de l'Injection de Dépendance (DI) est d'utiliser l'injection par constructeur ou un autre motif DI (mais l'injection par constructeur est la plus courante) pour injecter les dépendances dans le consommateur, indépendamment du conteneur DI.

Dans votre exemple, il semble que vous ayez besoin des dépendances TestSuite et TestCase, donc votre classe TestSuiteParser devrait annoncer statiquement qu'elle nécessite ces dépendances en les demandant dans son (unique) constructeur :

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

    // ...
}

Remarquez comment la combinaison du mot-clé readonly et de la Guard Clause protège les invariants de la classe, garantissant que les dépendances seront disponibles pour toute instance de TestSuiteParser créée avec succès.

Vous pouvez maintenant implémenter la méthode Parse de cette manière :

public TestSuite Parse(XPathNavigator someXml) 
{ 
    List 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 une fabrique abstraite au lieu d'un seul TestCase.)

Depuis votre Racine de Composition, vous pouvez configurer Unity (ou tout autre conteneur) :

container.RegisterType();
container.RegisterType();
container.RegisterType();

var parser = container.Resolve();

Lorsque le conteneur résout TestSuiteParser, il comprend le motif d'injection par constructeur, donc il Auto-Câble l'instance avec toutes ses dépendances requises.

Créer un conteneur Singleton ou passer le conteneur partout ne sont que deux variations de l'anti-pattern du Service Locator, donc je ne recommanderais pas cela.

13voto

BruceHill Points 2506

Je suis nouveau dans l'Injection de dépendance et j'avais également cette question. J'avais du mal à comprendre l'IDI, principalement parce que je me concentrais sur l'application de l'IDI à la seule classe sur laquelle je travaillais et une fois que j'avais ajouté les dépendances au constructeur, j'essayais immédiatement de trouver un moyen d'amener le conteneur Unity aux endroits où cette classe devait être instanciée afin de pouvoir appeler la méthode Résoudre sur la classe. En conséquence, je pensais à rendre le conteneur Unity globalement disponible en tant que statique ou à l'encapsuler 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" était cet article:

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

Et ce paragraphe en particulier a été le moment de "lumière" :

"99% de votre base de code ne devrait pas avoir connaissance de votre conteneur IoC. C'est uniquement la classe racine ou le programme d'amorçage qui utilise le conteneur et même alors, un seul appel à la résolution est généralement tout ce qui est nécessaire pour construire votre graphe de dépendances et démarrer l'application ou la requête."

Cet article m'a aidé à comprendre que je ne dois en fait pas accéder au conteneur Unity partout dans l'application, mais seulement à la racine de l'application. Je dois donc appliquer le principe de l'IDI de manière répétée jusqu'à la classe racine de l'application.

J'espère que cela aidera d'autres personnes aussi confuses que je l'étais! :)

4voto

Mattias Jakobsson Points 6101

Vous ne devriez pas vraiment avoir besoin d'utiliser directement votre conteneur dans de nombreux endroits de votre application. Vous devriez prendre toutes vos dépendances dans le constructeur et ne pas y accéder depuis vos méthodes. Votre exemple pourrait ressembler à ceci :

public class TestSuiteParser : ITestSuiteParser {
    private TestSuite testSuite;

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

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

        foreach (XPathNavigator blah in aListOfNodes)
        {
            //Je ne comprends pas ce que vous essayez de faire ici?
            TestCase testCase = ??? // Je veux obtenir ceci de mon conteneur Unity
            testSuite.TestCase.Add(testCase);
        } 
    }
}

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

Il existe également un projet intéressant appelé CommonServiceLocator. C'est un projet qui possède une interface partagée pour tous les conteneurs ioc populaires afin que vous n'ayez pas de dépendance sur un conteneur spécifique.

0voto

Dantte Points 663

Si seulement on pouvait avoir un "ServiceLocator" qui serait passé aux constructeurs de services, mais qui somehow arrive à "Déclarer" les dépendances prévues de la classe dans laquelle il est injecté (c'est-à-dire ne pas cacher les dépendances)... de cette façon, toutes les objections au pattern du localisateur de services pourraient être mises au repos.

public class MyBusinessClass
{
public MyBusinessClass(IServiceResolver locator)
{
//garder le résolveur pour une utilisation ultérieure
}
}

Malheureusement, cela n'existera évidemment que dans mes rêves, car en c#, les paramètres génériques variables sont interdits (encore), donc ajouter manuellement une nouvelle interface générique à chaque fois qu'un paramètre générique additionnel est nécessaire, serait malaisé.

Si, en revanche, on pouvait accomplir cela malgré la limitation de c# de la manière suivante...

public class MyBusinessClass
{
public MyBusinessClass(IServiceResolver>> locator)
{
//garder le résolveur pour une utilisation ultérieure
}
}

De cette façon, il suffit de faire un peu plus de saisie pour obtenir le même résultat. Ce qui n'est pas encore certain, c'est si, en concevant correctement la classe TArg (j'assume qu'un héritage astucieux sera utilisé pour permettre un nidification infinie des paramètres génériques de TArg), les conteneurs DI seront capables de résoudre correctement l'IServiceResolver. L'idée, en fin de compte, est simplement de passer autour de la même implémentation de l'IServiceResolver peu importe la déclaration générique trouvée dans le constructeur de la classe injectée.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X