web-dev-qa-db-fra.com

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

Je comprends que l'instanciation directe des dépendances à l'intérieur d'une classe est considérée comme une mauvaise pratique. Cela a du sens car le fait de coupler étroitement tout ce qui à son tour rend les tests très difficiles.

Presque tous les frameworks que j'ai rencontrés semblent favoriser l'injection de dépendances avec un conteneur plutôt que l'utilisation de localisateurs de services. Les deux semblent réaliser la même chose 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 plutôt que l'autre?

112
tom6025222

Lorsque l'objet lui-même est chargé de demander ses dépendances, au lieu de les accepter via un constructeur, il cache des informations essentielles. C'est légèrement mieux que le cas très étroitement couplé de l'utilisation de new pour instancier ses dépendances. Il réduit le couplage car vous pouvez en fait changer les dépendances qu'il obtient, mais il a toujours une dépendance qu'il ne peut pas secouer: le localisateur de service. Cela devient la chose dont tout dépend.

Un conteneur qui fournit des dépendances via des arguments de constructeur donne le plus de clarté. Nous voyons d'emblée qu'un objet a besoin à la fois d'un AccountRepository et d'un PasswordStrengthEvaluator. Lorsque vous utilisez un localisateur de services, ces informations sont moins immédiatement apparentes. Vous verriez tout de suite un cas où un objet a, oh, 17 dépendances, et vous vous diriez: "Hmm, cela semble beaucoup. Que se passe-t-il là-dedans?" Les appels à un localisateur de services peuvent être répartis entre les différentes méthodes et se cacher derrière une logique conditionnelle, et vous pourriez ne pas vous rendre compte que vous avez créé une "classe de Dieu" - une qui fait tout. Peut-être que cette classe pourrait être refactorisée en 3 classes plus petites qui sont plus ciblées et donc plus testables.

Envisagez maintenant de tester. Si un objet utilise un localisateur de services pour obtenir ses dépendances, votre infrastructure de test aura également besoin d'un localisateur de services. Dans un test, vous allez configurer le localisateur de services pour fournir les dépendances à l'objet testé - peut-être un FakeAccountRepository et un VeryForgivingPasswordStrengthEvaluator, puis exécutez 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 dépend également du localisateur de services. C'est une autre chose que vous devez configurer dans chaque test, ce qui rend l'écriture des tests moins attrayante.

Recherchez "Serivce Locator is an Anti-Pattern" pour l'article de Mark Seeman à ce sujet. Si vous êtes dans le monde .Net, procurez-vous son livre. C'est très bien.

126
Carl Raymond

Imaginez que vous êtes un travailleur dans une usine qui fabrique des chaussures .

Vous êtes responsable de l'assemblage des chaussures et vous aurez donc besoin de beaucoup de choses pour ce 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 n'avez pas encore de matériel ou d'outils.

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

Vous demandez au Service Locator chaque fois que vous avez besoin de quelque chose, et ils partent le trouver pour vous. Le Service Locator 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 demandez pas quelque chose d'inattendu. Si le localisateur n'a pas été informé à l'avance d'un outil ou d'un matériau particulier, il ne pourra pas vous l'obtenir et il vous haussera les épaules.

service locator

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

Au démarrage de l'usine, le Big Boss connu sous le nom de Racine de composition saisit le conteneur et remet tout au Gestionnaires hiérarchiques .

Les supérieurs hiérarchiques disposent désormais de ce dont ils ont besoin pour s'acquitter de leurs tâches quotidiennes. Ils prennent ce qu'ils ont et transmettent ce dont ils ont besoin à leurs subordonnés.

Ce processus se poursuit, avec des dépendances ruisselant sur la ligne 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.

Fondamentalement, dès que vous vous présentez au travail, tout ce dont vous avez besoin est déjà là dans une boîte qui vous attend. Vous n'aviez pas besoin de savoir comment les obtenir.

dependency injection container

79
Rowan Freeman

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 des besoins d'une classe. Les IDE modernes indiqueront quels arguments le constructeur accepte et leurs types. Si vous utilisez un localisateur de service, vous devez lire la classe avant de savoir quelles dépendances sont requises.
  • L'injection de dépendance semble adhérer davantage au principe "ne demandez pas" que les localisateurs de services. En exigeant qu'une dépendance soit d'un type spécifique, vous "dites" quelles sont les dépendances requises. Il est impossible d'instancier la classe sans passer les dépendances requises. Avec un localisateur de service, vous "demandez" un service et si le localisateur de service n'est pas configuré correctement, vous risquez de ne pas obtenir ce qui est requis.
10
tom6025222

Je viens en retard à cette fête mais je ne peux pas résister.

Quelle est la différence entre l'utilisation de l'injection de dépendances 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 quoi.

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

Est-ce à dire que si vous voulez éviter le localisateur de services, vous ne pouvez pas utiliser de conteneur? Non. Il suffit d’empêcher les clients de connaître le conteneur. La principale différence est l'endroit où vous utilisez le conteneur.

Disons que Client a besoin de 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 Client sait trouver Dependency. Bien sûr, il utilise un ClassPathXmlApplicationContext codé en dur, mais même si vous injectez que vous avez toujours un localisateur de service parce que Client appelle beanfactory.getBean().

Pour éviter le localisateur de service, vous n'avez pas à abandonner ce conteneur. Il suffit de le déplacer hors de Client pour que Client ne le sache pas.

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 que Client n'a désormais 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 principal où il peut créer un graphique d'objets de tous vos objets à longue durée de vie. Choisissez l'un de ces objets à extraire et appelez une méthode dessus et vous démarrez le graphique entier.

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 main sait toujours localiser les dépendances! Oui. Mais en ne diffusant pas ces connaissances, vous avez évité le problème principal du localisateur de services. La décision d'utiliser un conteneur est désormais prise en un seul endroit et pourrait être modifiée sans réécrire des centaines de clients.

5
candied_orange

Je pense que la façon la plus simple de comprendre la différence entre les deux et pourquoi un conteneur DI est tellement meilleur qu'un localisateur de services est de réfléchir à la raison pour laquelle nous faisons une inversion de dépendance en premier lieu.

Nous faisons une inversion de dépendances pour que chaque classe indique explicitement exactement de quoi elle dépend pour l'opération. 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 refactoring à 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 que nous avons besoin d'un IOutputProvider et de rien d'autre pour faire fonctionner cette classe. Ceci est entièrement testable et dépend d'une seule interface. Je peux déplacer cette classe n'importe où dans mon application, y compris un projet différent et tout ce dont il a besoin est l'accès à l'interface IOutputProvider. Si d'autres développeurs veulent ajouter quelque chose de nouveau à cette classe, ce qui nécessite une deuxième dépendance, ils doivent être explicites sur ce dont ils ont besoin dans le constructeur.

Jetez un œil à 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 comme dépendance. Voici les problèmes qui sont immédiatement évidents:

  • Le tout premier problème avec cela est que 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 encore 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. Il est caché dans une propriété de la chose que j'ai demandée. Maintenant, j'ai besoin d'accéder à la fois à ServiceLocator et à IOutputProvider si je veux déplacer la classe vers 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 se rend même pas compte qu'il le prend quand il ajoute 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 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 est implémenté par un service Web de très longue durée qui écrit la chaîne dans quinze bases de données différentes à travers le monde 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 rédigeons-nous le test?

Pour ce faire, nous devons effectuer une configuration sophistiquée dans la classe ServiceLocator statique pour 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 ce serait 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, vous vous retrouvez avec a) un test qui provoque des modifications de code intrusives dans la classe ServiceLocator non liée; ou b) aucun test du tout. Et vous vous retrouvez avec une solution moins flexible.

Ainsi, 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 précédemment. Le localisateur de services nécessite plus de code, indique aux autres développeurs qu'il a besoin de choses qu'il n'a pas, encourage les autres développeurs à écrire du code pire et nous donne 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é.

1
Stephen