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é .