Quelle est la différence entre l'injection de dépendance avec un conteneur et l'utilisation d'un localisateur de services?

107

Je comprends que l’instanciation directe des dépendances au sein d’une classe est considérée comme une mauvaise pratique. Cela a du sens car cela permet de coupler étroitement tout ce qui rend les tests très difficiles.

Presque tous les frameworks que j'ai rencontrés semblent préférer l'injection de dépendance avec un conteneur à l'utilisation de localisateurs de services. Les deux semblent obtenir le même résultat en permettant au programmeur de spécifier quel objet doit être renvoyé lorsqu'une classe nécessite une dépendance.

Quelle est la différence entre les deux? Pourquoi devrais-je choisir l'un sur l'autre?

tom6025222
la source
3
Cela a été demandé (et répondu) sur StackOverflow: stackoverflow.com/questions/8900710/…
Kyralessa
2
À mon avis, il n’est pas rare que les développeurs trompent les autres en leur faisant croire que leur localisateur de service est une injection de dépendance. Ils le font parce qu'on pense souvent que l'injection de dépendance est plus avancée.
Gherman le
1
Je suis surpris que personne n'ait mentionné qu'avec les localisateurs de services, il est plus difficile de savoir quand une ressource transitoire peut être détruite. J'ai toujours pensé que c'était la raison principale.
Buh Buh le

Réponses:

126

Lorsque l'objet lui-même est responsable de demander ses dépendances, par opposition à les accepter via un constructeur, il cache certaines informations essentielles. Ce n'est que légèrement meilleur que le cas très étroitement lié d'utilisation newd'instancier ses dépendances. Il réduit le couplage car vous pouvez en fait changer les dépendances qu'il génère, mais il a toujours une dépendance qu'il ne peut pas ébranler: le localisateur de service. Cela devient la chose dont tout dépend.

Un conteneur qui fournit des dépendances via des arguments de constructeur apporte le plus de clarté. Nous voyons dès le départ qu'un objet a besoin d'un AccountRepositoryet d'un PasswordStrengthEvaluator. Lorsque vous utilisez un service de localisation, cette information est moins apparente immédiatement. Vous voyez tout de suite le cas d'un objet qui a, oh, 17 dépendances, et vous dites: "Hmm, cela semble être beaucoup. Que se passe-t-il là-dedans?" Les appels à un localisateur de service peuvent être répartis autour des différentes méthodes et cachés derrière une logique conditionnelle. Vous ne réaliserez peut-être pas que vous avez créé une "classe de Dieu" - une classe qui fait tout. Peut-être que cette classe pourrait être refactorisée en 3 classes plus petites, plus ciblées et donc plus testables.

Considérons maintenant les tests. Si un objet utilise un localisateur de service pour obtenir ses dépendances, votre infrastructure de test aura également besoin d'un localisateur de service. Dans un test, vous allez configurer le localisateur de service pour fournir les dépendances à l'objet à tester (peut-être a FakeAccountRepositoryet a) VeryForgivingPasswordStrengthEvaluator, puis exécuter le test. Mais c'est plus de travail que de spécifier des dépendances dans le constructeur de l'objet. Et votre infrastructure de test devient également dépendante du localisateur de services. C'est une autre chose que vous devez configurer dans chaque test, ce qui rend l'écriture de tests moins attrayante.

Cherchez "Serivce Locator est un anti-modèle" pour l'article de Mark Seeman à ce sujet. Si vous êtes dans le monde .Net, procurez-vous son livre. C'est très bien.

Carl Raymond
la source
1
En lisant la question, j'ai pensé à Adaptive Code via C # de Gary McLean Hall, ce qui correspond assez bien à cette réponse. Il a quelques bonnes analogies, comme le localisateur de service, la clé d’un coffre-fort; remis à une classe, il peut créer à volonté des dépendances difficiles à repérer.
Dennis
10
Une chose a besoin de l' OMI à ajouter à la constructor supplied dependenciescontre service locatorest que l'ancien peut être verfied compilation, alors que celui - ci ne peut que être l' exécution vérifiée.
andras
2
De plus, un localisateur de service ne décourage pas un composant de posséder de nombreuses dépendances. Si vous devez ajouter 10 paramètres à votre constructeur, vous avez un bon sentiment que quelque chose ne va pas avec votre code. Toutefois, si vous passez 10 appels statiques à un localisateur de service, il n’est peut-être pas évident que vous ayez un problème. J'ai travaillé sur un grand projet avec un localisateur de services et c'était le plus gros problème. Il était bien trop facile de court-circuiter un nouveau chemin plutôt que de rester assis, de réfléchir, de revoir la conception et de refactoriser.
Pace
1
À propos des tests - But that's more work than specifying dependencies in the object's constructor.je voudrais objecter. Avec un localisateur de service, il vous suffit de spécifier les 3 dépendances dont vous avez réellement besoin pour votre test. Avec une DI basée sur le constructeur, vous devez spécifier TOUTES LES 10, même si 7 ne sont pas utilisées.
Vilx
4
@ Vilx- Si vous avez plusieurs paramètres de constructeur inutilisés, il est probable que votre classe enfreint le principe de responsabilité unique.
RB.
79

Imaginez que vous soyez un ouvrier dans une usine de chaussures .

Vous êtes responsable de l' assemblage des chaussures et vous aurez donc besoin de beaucoup de choses pour le faire.

  • Cuir
  • Mètre ruban
  • La colle
  • Ongles
  • Marteau
  • Les ciseaux
  • Les lacets

Etc.

Vous êtes au travail dans l'usine et vous êtes prêt à commencer. Vous avez une liste d'instructions sur la façon de procéder, mais vous ne disposez pas encore du matériel ou des outils.

Un localisateur de service est comme un contremaître qui peut vous aider à obtenir ce dont vous avez besoin.

Vous demandez au localisateur de services chaque fois que vous avez besoin de quelque chose, et ils partent à votre recherche. Le localisateur de services a été informé à l'avance de ce que vous êtes susceptible de demander et comment le trouver.

Vous feriez mieux d'espérer que vous ne demandiez pas quelque chose d'inattendu cependant. Si le localisateur n'a pas été informé à l'avance d'un outil ou d'un matériau en particulier, il ne pourra pas l'obtenir pour vous et il vous haussera les épaules.

localisateur de service

Un conteneur d'injection de dépendance (DI) est comme une grande boîte qui contient tout ce dont tout le monde a besoin au début de la journée.

Lorsque l’usine démarre, le Big Boss, connu sous le nom de Composition Root, saisit le conteneur et donne tout le contenu aux responsables de ligne .

Les responsables hiérarchiques ont maintenant ce dont ils ont besoin pour s’acquitter de leurs tâches de la journée. Ils prennent ce qu'ils ont et transmettent ce qui est nécessaire à leurs subordonnés.

Ce processus se poursuit, les dépendances se répercutant sur la chaîne de production. Finalement, un conteneur de matériaux et d'outils apparaît pour votre contremaître.

Votre contremaître distribue maintenant exactement ce dont vous avez besoin, à vous et aux autres travailleurs, sans même que vous le demandiez.

En gros, dès que vous vous présentez au travail, tout ce dont vous avez besoin se trouve déjà dans une boîte qui vous attend. Vous n'avez besoin de rien savoir sur la façon de les obtenir.

conteneur d'injection de dépendance

Rowan Freeman
la source
45
C’est une excellente description de chacune de ces 2 choses, de superbes diagrammes aussi! Mais cela ne répond pas à la question de "Pourquoi choisir l'un sur l'autre"?
Matthieu M.
21
"Vous feriez mieux d'espérer que vous ne demandiez pas quelque chose d'inattendu cependant. Si le localisateur n'a pas été informé à l'avance d'un outil ou d'un matériau en particulier"
Kenneth K.
5
@FrankHopkins Si vous omettez d'enregistrer une interface / classe avec votre conteneur DI, votre code sera toujours compilé et échouera rapidement au moment de l'exécution.
Kenneth K.
3
Les deux sont également susceptibles d'échouer, en fonction de la façon dont ils sont configurés. Mais avec un conteneur DI, il est plus probable que vous rencontriez un problème plus tôt, lorsque la classe X est construite, plutôt que plus tard, lorsque la classe X a réellement besoin de cette dépendance. C'est sans doute mieux, car il est plus facile de connaître le problème, de le trouver et de le résoudre. Je suis cependant d’accord avec Kenneth pour dire que la réponse suggère que ce problème existe uniquement pour les localisateurs de services, ce qui n’est certainement pas le cas.
GolezTrol le
3
Excellente réponse, mais je pense qu'il manque une autre chose. qui est, si vous demandez au contremaître à tout moment pour un éléphant, et il ne sais comment obtenir un, il obtiendra pour vous pas de questions , même si vous demanda: ne pas besoin. Et quelqu'un qui essaie de résoudre un problème sur le terrain (un développeur si vous préférez) se demandera si wtf est un éléphant agissant ici sur ce bureau, rendant ainsi plus difficile pour eux de raisonner sur ce qui ne va vraiment pas. Tandis qu'avec DI, vous, la classe ouvrière, déclarez à l'avance toutes les dépendances dont vous avez besoin avant même de commencer le travail, facilitant ainsi la réflexion.
Stephen Byrne le
10

Quelques points supplémentaires que j'ai trouvés en parcourant le Web:

  • L'injection de dépendances dans le constructeur facilite la compréhension de ce dont une classe a besoin. Les IDE modernes indiqueront quels arguments le constructeur accepte et leurs types. Si vous utilisez un localisateur de services, vous devez lire le cours avant de savoir quelles dépendances sont requises.
  • L'injection de dépendances semble adhérer davantage au principe "ne demandez pas" que les localisateurs de services. En imposant qu'une dépendance soit d'un type spécifique, vous «indiquez» les dépendances requises. Il est impossible d'instancier la classe sans passer les dépendances requises. Avec un localisateur de services, vous "demandez" un service et si le localisateur de services n'est pas configuré correctement, vous risquez de ne pas obtenir ce qui est requis.
tom6025222
la source
4

J'arrive en retard à cette soirée mais je ne peux pas résister.

Quelle est la différence entre l'injection de dépendance avec un conteneur et l'utilisation d'un localisateur de services?

Parfois pas du tout. Ce qui fait la différence, c'est ce qui sait de quoi.

Vous savez que vous utilisez un localisateur de service lorsque le client à la recherche de la dépendance connaît le conteneur. Un client sachant trouver ses dépendances, même lorsqu'il passe par un conteneur pour les obtenir, est le modèle de localisation de service.

Cela signifie-t-il que si vous souhaitez éviter le service de localisation, vous ne pouvez pas utiliser un conteneur? Non, vous devez juste empêcher les clients de connaître le conteneur. La principale différence réside dans l’utilisation du conteneur.

Permet de dire les Clientbesoins Dependency. Le conteneur a un Dependency.

class Client { 
    Client() { 
        BeanFactory beanfactory = new ClassPathXmlApplicationContext("Beans.xml");
        this.dependency = (Dependency) beanfactory.getBean("dependency");        
    }
    Dependency dependency;
}

Nous venons de suivre le modèle de localisateur de service car Clientsait comment le trouver Dependency. Bien sûr, il utilise un code dur, ClassPathXmlApplicationContextmais même si vous injectez que vous avez toujours un localisateur de service parce que des Clientappels beanfactory.getBean().

Pour éviter le service de localisation, vous n'avez pas à abandonner ce conteneur. Vous devez juste le sortir Clientafin de Clientne pas le savoir.

class EntryPoint { 
    public static void main(String[] args) {
        BeanFactory beanfactory = new ClassPathXmlApplicationContext("Beans.xml");
        Client client = (Client) beanfactory.getBean("client");

        client.start();
    }
}

<?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="dependency" class="Dependency">
    </bean>

    <bean id="client" class="Client">
        <constructor-arg value="dependency" />        
    </bean>
</beans>

Remarquez comment Clientmaintenant n’a aucune idée que le conteneur existe:

class Client { 
    Client(Dependency dependency) { 

        this.dependency = dependency;        
    }
    Dependency dependency;
}

Déplacez le conteneur de tous les clients et collez-le dans le répertoire principal où il peut créer un graphe d'objets de tous vos objets de longue durée. Choisissez l'un de ces objets à extraire, appelez une méthode dessus et commencez à compter tout le graphique.

Cela déplace toute la construction statique dans les conteneurs XML tout en gardant tous vos clients parfaitement ignorants de la façon de trouver leurs dépendances.

Mais principal sait toujours comment localiser les dépendances! Oui. Mais en ne diffusant pas ces informations, vous avez évité le problème fondamental du localisateur de services. La décision d'utiliser un conteneur est maintenant prise à un endroit et peut être modifiée sans réécrire des centaines de clients.

confits_orange
la source
1

Je pense que la meilleure façon de comprendre la différence entre les deux et la raison pour laquelle un conteneur DI est tellement préférable à un localisateur de service est de réfléchir à la raison pour laquelle nous faisons l'inversion de dépendance.

Nous faisons l'inversion de dépendance de sorte que chaque classe énonce explicitement de quoi elle dépend pour son fonctionnement. Nous le faisons parce que cela crée le couplage le plus lâche possible. Plus le couplage est lâche, plus il est facile de tester et de refactoriser (et nécessite généralement le moins de refactorisation à l'avenir car le code est plus propre).

Regardons la classe suivante:

public class MySpecialStringWriter
{
  private readonly IOutputProvider outputProvider;
  public MySpecialFormatter(IOutputProvider outputProvider)
  {
    this.outputProvider = outputProvider;
  }

  public void OutputString(string source)
  {
    this.outputProvider.Output("This is the string that was passed: " + source);
  }
}

Dans cette classe, nous déclarons explicitement qu'il nous faut un IOutputProvider et rien d'autre pour que cette classe fonctionne. Ceci est entièrement testable et dépend d'une interface unique. Je peux déplacer cette classe n'importe où dans mon application, y compris un projet différent. Tout ce dont elle a besoin est un accès à l'interface IOutputProvider. Si d'autres développeurs souhaitent ajouter quelque chose de nouveau à cette classe, qui nécessite une deuxième dépendance, ils doivent être explicites sur ce dont ils ont besoin dans le constructeur.

Regardez la même classe avec un localisateur de services:

public class MySpecialStringWriter
{
  private readonly ServiceLocator serviceLocator;
  public MySpecialFormatter(ServiceLocator serviceLocator)
  {
    this.serviceLocator = serviceLocator;
  }

  public void OutputString(string source)
  {
    this.serviceLocator.OutputProvider.Output("This is the string that was passed: " + source);
  }
}

Maintenant, j'ai ajouté le localisateur de service en tant que dépendance. Voici les problèmes qui sont immédiatement évidents:

  • Le tout premier problème avec cela est qu'il faut plus de code pour obtenir le même résultat. Plus de code est mauvais. Ce n'est pas beaucoup plus de code mais c'est toujours plus.
  • Le deuxième problème est que ma dépendance n’est plus explicite . J'ai encore besoin d'injecter quelque chose dans la classe. Sauf que maintenant la chose que je veux n'est pas explicite. C'est caché dans une propriété de la chose que j'ai demandée. J'ai maintenant besoin d'accéder à ServiceLocator et à IOutputProvider si je veux déplacer la classe dans un autre assembly.
  • Le troisième problème est qu'une dépendance supplémentaire peut être prise par un autre développeur qui ne réalise même pas qu'ils la prennent quand ils ajoutent du code à la classe.
  • Enfin, ce code est plus difficile à tester (même si ServiceLocator est une interface) car nous devons nous moquer de ServiceLocator et de IOutputProvider au lieu de simplement IOutputProvider.

Alors, pourquoi ne faisons-nous pas du localisateur de services une classe statique? Nous allons jeter un coup d'oeil:

public class MySpecialStringWriter
{
  public void OutputString(string source)
  {
    ServiceLocator.OutputProvider.Output("This is the string that was passed: " + source);
  }
}

C'est beaucoup plus simple, non?

Faux.

Supposons que IOutputProvider soit implémenté par un service Web très long qui écrit la chaîne dans quinze bases de données différentes dans le monde entier et prend beaucoup de temps.

Essayons de tester cette classe. Nous avons besoin d'une implémentation différente de IOutputProvider pour le test. Comment écrivons-nous le test?

Pour ce faire, nous devons procéder à une configuration sophistiquée dans la classe statique ServiceLocator afin d’utiliser une implémentation différente de IOutputProvider lorsqu’elle est appelée par le test. Même écrire cette phrase était douloureux. Sa mise en œuvre serait tortueuse et constituerait un cauchemar de maintenance . Nous ne devrions jamais avoir besoin de modifier une classe spécifiquement pour les tests, surtout si cette classe n'est pas la classe que nous essayons réellement de tester.

Alors maintenant, il vous reste soit a) un test qui provoque des modifications de code intrusives dans la classe ServiceLocator non liée; ou b) pas de test du tout. Et vous vous retrouvez également avec une solution moins flexible.

Donc, la classe de localisateur de service doit être injectée dans le constructeur. Ce qui signifie que nous nous retrouvons avec les problèmes spécifiques mentionnés plus haut. Le localisateur de service nécessite plus de code, indique aux autres développeurs qu'il a besoin de choses inutiles, encourage les autres développeurs à écrire du code moins performant et nous offre moins de flexibilité pour aller de l'avant.

En termes simples, les localisateurs de services augmentent le couplage dans une application et encouragent les autres développeurs à écrire du code hautement couplé .

Stephen
la source
Les localisateurs de service (SL) et l’injection de dépendance (DI) résolvent le même problème de la même manière. La seule différence si vous êtes "en train de" dire "DI ou" demander "à SL pour des dépendances.
Matthew Whited
@MatthewWhited La principale différence est le nombre de dépendances implicites que vous prenez avec un localisateur de service. Et cela fait une énorme différence pour la maintenabilité et la stabilité à long terme du code.
Stephen
C'est vraiment pas. Vous avez le même nombre de dépendances dans les deux cas.
Matthew Whited
Au départ, oui, mais vous pouvez créer des dépendances avec un localisateur de service sans vous rendre compte que vous en prenez. Ce qui élimine en grande partie la raison pour laquelle nous faisons l’inversion de dépendance. Une classe dépend des propriétés A et B, une autre dépend de B et C. Soudain, le localisateur de services devient une classe de dieu. Le conteneur DI ne fonctionne pas et les deux classes ne dépendent que de A et B et C et C respectivement. Cela facilite leur refactorisation cent fois. Les localisateurs de services sont insidieux en ce qu'ils apparaissent identiques à DI, mais ils ne le sont pas.
Stephen