ServiceLocator est-il un anti-pattern?

137

Récemment, j'ai lu l'article de Mark Seemann sur l'anti-pattern Service Locator.

L'auteur souligne deux raisons principales pour lesquelles ServiceLocator est un anti-pattern:

  1. Problème d'utilisation de l'API (avec lequel je suis parfaitement d'accord)
    Lorsque la classe utilise un localisateur de service, il est très difficile de voir ses dépendances car, dans la plupart des cas, la classe n'a qu'un seul constructeur PARAMETERLESS. Contrairement à ServiceLocator, l'approche DI expose explicitement les dépendances via les paramètres du constructeur afin que les dépendances soient faciles à voir dans IntelliSense.


  2. Problème de maintenance (qui me laisse perplexe) Considérez l'exemple suivant

Nous avons une classe 'MyType' qui utilise une approche de localisateur de service:

public class MyType
{
    public void MyMethod()
    {
        var dep1 = Locator.Resolve<IDep1>();
        dep1.DoSomething();
    }
}

Maintenant, nous voulons ajouter une autre dépendance à la classe 'MyType'

public class MyType
{
    public void MyMethod()
    {
        var dep1 = Locator.Resolve<IDep1>();
        dep1.DoSomething();

        // new dependency
        var dep2 = Locator.Resolve<IDep2>();
        dep2.DoSomething();
    }
}

Et c'est ici que commence mon malentendu. L'auteur dit:

Il devient beaucoup plus difficile de dire si vous introduisez un changement radical ou non. Vous devez comprendre toute l'application dans laquelle le localisateur de services est utilisé et le compilateur ne vous aidera pas.

Mais attendez une seconde, si nous utilisions l'approche DI, nous introduirions une dépendance avec un autre paramètre dans le constructeur (en cas d'injection de constructeur). Et le problème sera toujours là. Si nous oublions de configurer ServiceLocator, nous pouvons oublier d'ajouter un nouveau mappage dans notre conteneur IoC et l'approche DI aurait le même problème d'exécution.

En outre, l'auteur a mentionné les difficultés des tests unitaires. Mais n'aurons-nous pas de problèmes avec l'approche DI? N'aurons-nous pas besoin de mettre à jour tous les tests qui instanciaient cette classe? Nous les mettrons à jour pour passer une nouvelle dépendance simulée juste pour rendre notre test compilable. Et je ne vois aucun avantage à cette mise à jour et au temps passé.

Je n'essaye pas de défendre l'approche de Service Locator. Mais ce malentendu me fait penser que je perds quelque chose de très important. Quelqu'un pourrait-il dissiper mes doutes?

MISE À JOUR (RÉSUMÉ):

La réponse à ma question "Service Locator est-il un anti-pattern" dépend vraiment des circonstances. Et je ne suggérerais certainement pas de le rayer de votre liste d'outils. Cela peut devenir très pratique lorsque vous commencez à utiliser du code hérité. Si vous avez la chance d'être au tout début de votre projet, l'approche DI peut être un meilleur choix car elle présente certains avantages par rapport à Service Locator.

Et voici les principales différences qui m'ont convaincu de ne pas utiliser Service Locator pour mes nouveaux projets:

  • Le plus évident et le plus important: Service Locator masque les dépendances de classe
  • Si vous utilisez un conteneur IoC, il analysera probablement tous les constructeurs au démarrage pour valider toutes les dépendances et vous donner un retour immédiat sur les mappages manquants (ou une mauvaise configuration); ce n'est pas possible si vous utilisez votre conteneur IoC comme localisateur de service

Pour plus de détails, lisez les excellentes réponses données ci-dessous.

Davidoff
la source
"N'aurons-nous pas besoin de mettre à jour tous les tests qui instanciaient cette classe?" Pas nécessairement vrai si vous utilisez un générateur dans vos tests. Dans ce cas, il vous suffirait de mettre à jour le générateur.
Peter Karlsson
Tu as raison, ça dépend. Dans les grandes applications Android, par exemple, jusqu'à présent, les gens ont été très réticents à utiliser la DI en raison de problèmes de performances sur les appareils mobiles à faible spécification. Dans de tels cas, vous devez trouver une alternative pour toujours écrire du code testable, et je dirais que Service Locator est un substitut assez bon dans ce cas. (Remarque: les choses peuvent changer pour Android lorsque le nouveau framework Dagger 2.0 DI est suffisamment mature.)
G. Lombard
1
Notez que depuis que cette question a été publiée, il y a une mise à jour sur Service Locator de Mark Seemann est un article Anti-Pattern décrivant comment le localisateur de service viole la POO en cassant l'encapsulation, qui est son meilleur argument à ce jour (et la raison sous-jacente de tous les symptômes qu'il a utilisé dans tous les arguments précédents). Mise à jour 2015-10-26: Le problème fondamental avec Service Locator est qu'il viole l'encapsulation .
NightOwl888

Réponses:

125

Si vous définissez des motifs comme des anti-motifs simplement parce qu'il y a des situations où cela ne correspond pas, alors OUI c'est un anti-motif. Mais avec ce raisonnement, tous les modèles seraient également des anti-modèles.

Au lieu de cela, nous devons vérifier s'il existe des utilisations valides des modèles, et pour Service Locator, il existe plusieurs cas d'utilisation. Mais commençons par regarder les exemples que vous avez donnés.

public class MyType
{
    public void MyMethod()
    {
        var dep1 = Locator.Resolve<IDep1>();
        dep1.DoSomething();

        // new dependency
        var dep2 = Locator.Resolve<IDep2>();
        dep2.DoSomething();
    }
}

Le cauchemar de maintenance avec cette classe est que les dépendances sont cachées. Si vous créez et utilisez cette classe:

var myType = new MyType();
myType.MyMethod();

Vous ne comprenez pas qu'il a des dépendances si elles sont masquées à l'aide de l'emplacement du service. Maintenant, si nous utilisons plutôt l'injection de dépendances:

public class MyType
{
    public MyType(IDep1 dep1, IDep2 dep2)
    {
    }

    public void MyMethod()
    {
        dep1.DoSomething();

        // new dependency
        dep2.DoSomething();
    }
}

Vous pouvez directement repérer les dépendances et ne pouvez pas utiliser les classes avant de les satisfaire.

Dans une application métier type, vous devez éviter d'utiliser l'emplacement de service pour cette raison même. Ce devrait être le modèle à utiliser lorsqu'il n'y a pas d'autres options.

Le motif est-il un anti-motif?

Non.

Par exemple, l'inversion des conteneurs de contrôle ne fonctionnerait pas sans l'emplacement du service. C'est ainsi qu'ils résolvent les services en interne.

Mais un bien meilleur exemple est ASP.NET MVC et WebApi. Selon vous, qu'est-ce qui rend possible l'injection de dépendances dans les contrôleurs? C'est vrai - l'emplacement du service.

Vos questions

Mais attendez une seconde, si nous utilisions l'approche DI, nous introduirions une dépendance avec un autre paramètre dans le constructeur (en cas d'injection de constructeur). Et le problème sera toujours là.

Il y a deux problèmes plus graves:

  1. Avec l'emplacement de service, vous ajoutez également une autre dépendance: le localisateur de service.
  2. Comment savoir quelle durée de vie les dépendances devraient avoir, et comment / quand elles doivent être nettoyées?

Avec l'injection de constructeur à l'aide d'un conteneur, vous obtenez cela gratuitement.

Si nous oublions de configurer ServiceLocator, nous pouvons oublier d'ajouter un nouveau mappage dans notre conteneur IoC et l'approche DI aurait le même problème d'exécution.

C'est vrai. Mais avec l'injection de constructeur, vous n'avez pas besoin d'analyser toute la classe pour déterminer quelles dépendances sont manquantes.

Et certains meilleurs conteneurs valident également toutes les dépendances au démarrage (en analysant tous les constructeurs). Donc, avec ces conteneurs, vous obtenez l'erreur d'exécution directement, et non à un moment temporel ultérieur.

En outre, l'auteur a mentionné les difficultés des tests unitaires. Mais, n'aurons-nous pas de problèmes avec l'approche DI?

Non. Comme vous n'avez pas de dépendance à un localisateur de service statique. Avez-vous essayé de faire fonctionner des tests parallèles avec des dépendances statiques? Ce n'est pas amusant.

Jgauffin
la source
2
Jgauffin, merci pour votre réponse. Vous avez souligné une chose importante concernant la vérification automatique au démarrage. Je n'y ai pas pensé et maintenant je vois un autre avantage de DI. Vous avez également donné un exemple: "var myType = new MyType ()". Mais je ne peux pas le considérer comme valide car nous n'instancions jamais les dépendances dans une vraie application (IoC Container le fait pour nous tout le temps). Ie: dans l'application MVC, nous avons un contrôleur, qui dépend de IMyService et MyServiceImpl dépend de IMyRepository. Nous n'instancierons jamais MyRepository et MyService. Nous obtenons des instances des paramètres Ctor (comme de ServiceLocator) et les utilisons. Pas nous?
davidoff
33
Votre seul argument pour que Service Locator ne soit pas un anti-modèle est: "l'inversion des conteneurs de contrôle ne fonctionnerait pas sans l'emplacement du service". Cet argument est cependant invalide, puisque Service Locator concerne les intentions et non la mécanique, comme expliqué clairement ici par Mark Seemann: "Un conteneur DI encapsulé dans une Composition Root n'est pas un Service Locator - c'est un composant d'infrastructure."
Steven
4
L'API Web @jgauffin n'utilise pas l'emplacement du service pour la DI dans les contrôleurs. Il ne fait pas du tout DI. Ce qu'il fait est: Il vous donne la possibilité de créer votre propre ControllerActivator pour passer aux services de configuration. À partir de là, vous pouvez créer une racine de composition, que ce soit Pure DI ou Containers. En outre, vous mélangez l'utilisation de Service Location, en tant que composant de votre modèle, avec la définition du «Service Locator Pattern». Avec cette définition, Composition Root DI pourrait être considérée comme un "Service Locator Pattern". Donc, tout l'intérêt de cette définition est sans objet.
Suamere
1
Je tiens à souligner que cette réponse est généralement bonne, je viens de commenter un point trompeur que vous avez soulevé.
Suamere
2
@jgauffin DI et SL sont tous deux des rendus de version d'IoC. SL n'est pas la bonne façon de procéder. Un conteneur IoC peut être SL ou utiliser DI. Cela dépend de la façon dont il est câblé. Mais SL est mauvais, mauvais mauvais. C'est une façon de cacher le fait que vous associez étroitement tout.
MirroredFate
37

Je tiens également à souligner que SI vous refactorisez le code hérité, le modèle Service Locator n'est pas seulement non pas un anti-modèle, mais c'est aussi une nécessité pratique. Personne ne va jamais agiter une baguette magique sur des millions de lignes de code et tout à coup tout ce code sera prêt pour la DI. Donc, si vous voulez commencer à introduire DI dans une base de code existante, il arrive souvent que vous changiez les choses pour devenir des services DI lentement, et le code qui référence ces services ne sera souvent PAS des services DI. Par conséquent, CES services devront utiliser le localisateur de services afin d'obtenir des instances de ces services qui ONT été convertis pour utiliser DI.

Ainsi, lors de la refactorisation de grandes applications héritées pour commencer à utiliser des concepts DI, je dirais que non seulement Service Locator n'est PAS un anti-modèle, mais que c'est le seul moyen d'appliquer progressivement les concepts DI à la base de code.

jwells131313
la source
12
Lorsque vous avez affaire à du code hérité, tout est justifié pour vous sortir de ce gâchis, même si cela implique de prendre des mesures intermédiaires (et imparfaites). Le localisateur de service est une telle étape intermédiaire. Cela vous permet de sortir de cet enfer une étape à la fois, tant que vous vous souvenez qu'il existe une bonne solution alternative qui est documentée, reproductible et éprouvée . Cette solution alternative est l'injection de dépendances et c'est précisément pourquoi le localisateur de service est toujours un anti-modèle; un logiciel correctement conçu ne l'utilise pas.
Steven
RE: "Quand vous avez affaire à du code hérité, tout est justifié pour vous sortir de ce gâchis" Parfois, je me demande s'il n'y a qu'un petit bout de code hérité qui ait jamais existé, mais parce que nous pouvons justifier quoi que ce soit pour le réparer, nous en quelque sorte jamais réussi à le faire.
Drew Delano
8

Du point de vue des tests, Service Locator est mauvais. Voir la belle explication Google Tech Talk de Misko Hevery avec des exemples de code http://youtu.be/RlfLCWKxHJ0 à partir de 8h45. J'ai aimé son analogie: si vous avez besoin de 25 $, demandez directement de l'argent plutôt que de donner votre portefeuille d'où l'argent sera pris. Il compare également Service Locator à une botte de foin qui contient l'aiguille dont vous avez besoin et sait comment la récupérer. Les classes utilisant Service Locator sont difficiles à réutiliser à cause de cela.

user3511397
la source
10
Ceci est une opinion recyclée et aurait été mieux comme commentaire. De plus, je pense que votre (son?) Analogie sert à prouver que certains modèles sont plus appropriés pour certains problèmes que pour d'autres.
8bitjunkie
6

Problème de maintenance (qui me laisse perplexe)

Il y a 2 raisons différentes pour lesquelles l'utilisation du localisateur de services est mauvaise à cet égard.

  1. Dans votre exemple, vous codez en dur une référence statique au localisateur de services dans votre classe. Cela couple étroitement votre classe directement au localisateur de services, ce qui signifie qu'il ne fonctionnera pas sans le localisateur de services . De plus, vos tests unitaires (et toute autre personne qui utilise la classe) dépendent également implicitement du localisateur de services. Une chose qui semble passer inaperçue ici est que lors de l'utilisation de l'injection de constructeur, vous n'avez pas besoin d'un conteneur DI lors des tests unitaires , ce qui simplifie considérablement vos tests unitaires (et la capacité des développeurs à les comprendre). C'est l'avantage des tests unitaires que vous obtenez en utilisant l'injection de constructeur.
  2. Quant à savoir pourquoi le constructeur Intellisense est important, les gens ici semblent avoir complètement manqué le point. Une classe est écrite une fois, mais elle peut être utilisée dans plusieurs applications (c'est-à-dire dans plusieurs configurations DI) . Au fil du temps, cela rapporte des dividendes si vous pouvez regarder la définition du constructeur pour comprendre les dépendances d'une classe, plutôt que de consulter la documentation (espérons-le à jour) ou, à défaut, de revenir au code source d'origine (qui pourrait ne pas être pratique) pour déterminer quelles sont les dépendances d'une classe. La classe avec le localisateur de services est généralement plus facile à écrire , mais vous payez plus que le coût de cette commodité dans la maintenance continue du projet.

Clair et simple: une classe avec un localisateur de services est plus difficile à réutiliser qu'une classe qui accepte ses dépendances via son constructeur.

Considérez le cas où vous devez utiliser un service à partir duquel LibraryAson auteur a décidé d'utiliser ServiceLocatorAet un service LibraryBdont l'auteur a décidé de l'utiliser ServiceLocatorB. Nous n'avons pas d'autre choix que d'utiliser 2 localisateurs de services différents dans notre projet. Le nombre de dépendances à configurer est un jeu de devinettes si nous n'avons pas une bonne documentation, un code source ou l'auteur sur la numérotation rapide. En cas d'échec de ces options, nous devrons peut-être utiliser un décompilateur justepour comprendre quelles sont les dépendances. Nous pouvons avoir besoin de configurer 2 API de localisation de services entièrement différentes, et selon la conception, il peut ne pas être possible de simplement envelopper votre conteneur DI existant. Il peut ne pas être possible du tout de partager une instance d'une dépendance entre les deux bibliothèques. La complexité du projet pourrait même être encore aggravée si les localisateurs de services ne résident pas réellement dans les mêmes bibliothèques que les services dont nous avons besoin - nous faisons implicitement glisser des références de bibliothèques supplémentaires dans notre projet.

Considérons maintenant les deux mêmes services réalisés avec l'injection de constructeur. Ajoutez une référence à LibraryA. Ajoutez une référence à LibraryB. Fournissez les dépendances dans votre configuration DI (en analysant ce qui est nécessaire via Intellisense). Terminé.

Mark Seemann a une réponse StackOverflow qui illustre clairement cet avantage sous forme graphique , qui s'applique non seulement lors de l'utilisation d'un localisateur de services d'une autre bibliothèque, mais également lors de l'utilisation de valeurs par défaut étrangères dans les services.

NightOwl888
la source
1

Mes connaissances ne sont pas assez bonnes pour en juger, mais en général, je pense que si quelque chose a une utilité dans une situation particulière, cela ne signifie pas nécessairement qu'il ne peut pas être un anti-modèle. Surtout, lorsque vous traitez avec des bibliothèques tierces, vous n'avez pas un contrôle total sur tous les aspects et vous risquez de finir par utiliser la meilleure solution.

Voici un paragraphe de Adaptive Code via C # :

"Malheureusement, le localisateur de services est parfois un anti-modèle incontournable. Dans certains types d'applications, en particulier Windows Workflow Foundation, l'infrastructure ne se prête pas à l'injection de constructeur. Dans ces cas, la seule alternative est d'utiliser un localisateur de services. C'est mieux que de ne pas injecter de dépendances du tout. Pour tout mon vitriol contre le (anti-) motif, c'est infiniment mieux que de construire manuellement des dépendances. Après tout, cela permet toujours les points d'extension très importants fournis par les interfaces qui permettent aux décorateurs, aux adaptateurs, et des avantages similaires. "

- Hall, Gary McLean. Code adaptatif via C #: codage agile avec des modèles de conception et des principes SOLID (Référence développeur) (p. 309). Pearson Education.

Siavash Mortazavi
la source
0

L'auteur raisonne que "le compilateur ne vous aidera pas" - et c'est vrai. Lorsque vous daignerez une classe, vous voudrez bien choisir son interface - entre autres objectifs pour la rendre aussi indépendante que ... car cela a du sens.

En demandant au client d'accepter la référence à un service (à une dépendance) via une interface explicite, vous

  • vérifier implicitement, donc le compilateur "aide".
  • Vous supprimez également le besoin pour le client de savoir quelque chose sur le "Locator" ou des mécanismes similaires, de sorte que le client est en fait plus indépendant.

Vous avez raison de dire que DI a ses problèmes / inconvénients, mais les avantages mentionnés les dépassent de loin ... IMO. Vous avez raison, avec DI, il y a une dépendance introduite dans l'interface (constructeur) - mais c'est, espérons-le, la dépendance même dont vous avez besoin et que vous voulez rendre visible et vérifiable.

Zrin
la source
Zrin, merci pour vos pensées. Si je comprends bien, avec une approche DI "appropriée", je ne devrais pas instancier mes dépendances ailleurs que dans les tests unitaires. Donc, le compilateur ne m'aidera que pour mes tests. Mais comme je l'ai décrit dans ma question initiale, cette "aide" avec des tests cassés ne me donne rien. Le fait-il?
davidoff
L'argument 'information statique' / 'vérification à la compilation' est un homme de paille. Comme @davidoff l'a souligné, DI est également sensible aux erreurs d'exécution. J'ajouterais également que les IDE modernes fournissent des vues infobulles des informations de commentaire / résumé pour les membres, et même dans ceux qui ne le font pas, quelqu'un continuera à consulter la documentation jusqu'à ce qu'il `` connaisse '' l'API. La documentation est une documentation, qu'il s'agisse d'un paramètre de constructeur obligatoire ou d'une remarque sur la façon de configurer une dépendance.
tuespetre
Penser l'implémentation / le codage et l'assurance qualité - la lisibilité du code est cruciale - en particulier pour la définition d'interface. Si vous pouvez vous passer des vérifications automatiques du temps de compilation et si vous commentez / documentez correctement votre interface, alors je suppose que vous pouvez au moins partiellement remédier aux inconvénients de la dépendance cachée à une sorte de variable globale avec un contenu pas facilement visible / prévisible. Je dirais que vous avez besoin de bonnes raisons d'utiliser ce modèle qui l'emportent sur ces inconvénients.
Zrin
0

Oui, le localisateur de service est un anti-modèle, il viole l'encapsulation et le solide .

Alireza Rahmani Khalili
la source
1
Je comprends ce que vous voulez dire mais il serait utile d'ajouter plus de détails, par exemple pourquoi il enfreint SOLID
reggaeguitar