J'ai une interface appelée IContext
. Aux fins de cela, peu importe ce qu'il fait, sauf ce qui suit:
T GetService<T>();
Cette méthode consiste à examiner le conteneur DI actuel de l'application et tente de résoudre la dépendance. Assez standard je pense.
Dans mon application ASP.NET MVC, mon constructeur ressemble à ceci.
protected MyControllerBase(IContext ctx)
{
TheContext = ctx;
SomeService = ctx.GetService<ISomeService>();
AnotherService = ctx.GetService<IAnotherService>();
}
Donc, plutôt que d'ajouter plusieurs paramètres dans le constructeur pour chaque service (car cela deviendra vraiment ennuyeux et long pour les développeurs qui étendent l'application), j'utilise cette méthode pour obtenir des services.
Maintenant, ça ne va pas . Cependant, la façon dont je le justifie actuellement dans ma tête est la suivante - je peux me moquer .
Je peux. Il ne serait pas difficile de se moquer IContext
pour tester le contrôleur. Je devrais quand même:
public class MyMockContext : IContext
{
public T GetService<T>()
{
if (typeof(T) == typeof(ISomeService))
{
// return another mock, or concrete etc etc
}
// etc etc
}
}
Mais comme je l'ai dit, ça ne va pas. Toutes pensées / abus bienvenus.
la source
public SomeClass(Context c)
. Ce code est assez clair, n'est-ce pas? Il déclare,that SomeClass
dépend d'unContext
. Euh, mais attendez, ce n'est pas le cas! Il ne dépend que de la dépendanceX
qu'il obtient du contexte. Cela signifie que chaque fois que vous y apportez une modification,Context
celle - ci peut casserSomeObject
, même si vous ne modifiez que l'Context
artY
. Mais oui, tu sais que tu n'as changé queY
nonX
, donc çaSomeClass
va. Mais écrire un bon code ne concerne pas ce que vous savez, mais ce que le nouvel employé sait quand il regarde votre code la première fois.Réponses:
Avoir un au lieu de plusieurs paramètres dans le constructeur n'est pas la partie problématique de cette conception . Tant que votre
IContext
classe n'est rien d'autre qu'une façade de service , spécialement pour fournir les dépendances utilisées dansMyControllerBase
, et non un localisateur de service général utilisé dans tout votre code, cette partie de votre code est IMHO ok.Votre premier exemple peut être changé en
ce ne serait pas un changement de conception substantiel de
MyControllerBase
. Si cette conception est bonne ou mauvaise ne dépend que du fait si vous voulezTheContext
,SomeService
etAnotherService
sont toujours tous initialisés avec des objets fictifs, ou tous avec des objets réelsDonc, utiliser un seul paramètre au lieu de 3 dans le constructeur peut être tout à fait raisonnable.
Ce qui pose problème, c'est d'
IContext
exposer laGetService
méthode en public. À mon humble avis, vous devriez éviter cela, mais plutôt garder les "méthodes d'usine" explicites. Est-ce que ce sera ok pour implémenter les méthodesGetSomeService
etGetAnotherService
de mon exemple en utilisant un localisateur de service? À mon humble avis, cela dépend. Tant que laIContext
classe continue d'être une simple fabrique abstraite dans le but spécifique de fournir une liste explicite d'objets de service, ce qui est acceptable à mon humble avis. Les usines abstraites ne sont généralement que du code «collé», qui n'a pas besoin d'être testé lui-même. Néanmoins, vous devriez vous demander si dans le contexte de méthodes commeGetSomeService
, si vous avez vraiment besoin d'un localisateur de service, ou si un appel de constructeur explicite ne serait pas plus simple.Alors, méfiez-vous, lorsque vous vous en tenez à une conception où l'
IContext
implémentation n'est qu'un wrapper autour d'uneGetService
méthode générique publique , permettant de résoudre toutes les dépendances arbitraires par des classes arbitraires, alors tout s'applique à ce que @BenjaminHodgson a écrit dans sa réponse.la source
GetService
méthode générique . Le refactoring vers des méthodes explicitement nommées et typées est préférable. Mieux encore serait d'exposer explicitement les dépendances de l'IContext
implémentation dans le constructeur.IValidatableObject
?Cette conception est connue sous le nom de Service Locator * et je ne l'aime pas. Il y a beaucoup d'arguments contre:
Service Locator vous couple à votre conteneur . En utilisant l'injection de dépendance régulière (où le constructeur énonce explicitement les dépendances), vous pouvez remplacer simplement votre conteneur par un autre, ou revenir à
new
-expressions. Avec votre,IContext
ce n'est pas vraiment possible.Service Locator masque les dépendances . En tant que client, il est très difficile de dire ce dont vous avez besoin pour construire une instance d'une classe. Vous avez besoin d'une sorte de
IContext
, mais vous devez également définir le contexte pour renvoyer les objets corrects afin de faire leMyControllerBase
travail. Ce n'est pas du tout évident de la signature du constructeur. Avec DI régulier, le compilateur vous dit exactement ce dont vous avez besoin. Si votre classe a beaucoup de dépendances, vous devriez ressentir cette douleur car elle vous incitera à refactoriser. Service Locator cache les problèmes liés aux mauvaises conceptions.Service Locator provoque des erreurs d'exécution . Si vous appelez
GetService
avec un paramètre de type incorrect, vous obtiendrez une exception. En d'autres termes, votreGetService
fonction n'est pas une fonction totale. (Les fonctions totales sont une idée du monde FP, mais cela signifie essentiellement que les fonctions doivent toujours renvoyer une valeur.) Mieux vaut laisser le compilateur vous aider et vous dire quand vous avez des dépendances erronées.Service Locator viole le principe de substitution Liskov . Parce que son comportement varie en fonction de l'argument type, Service Locator peut être vu comme s'il avait effectivement un nombre infini de méthodes sur l'interface! Cet argument est expliqué en détail ici .
Le localisateur de service est difficile à tester . Vous avez donné un exemple de faux
IContext
pour les tests, ce qui est bien, mais il est certainement préférable de ne pas avoir à écrire ce code en premier lieu. Injectez simplement vos fausses dépendances directement, sans passer par votre localisateur de services.Bref, juste faites pas . Cela semble être une solution séduisante au problème des classes avec beaucoup de dépendances, mais à long terme, vous allez juste rendre votre vie misérable.
* Je définis Service Locator comme un objet avec une
Resolve<T>
méthode générique qui est capable de résoudre des dépendances arbitraires et est utilisé dans toute la base de code (pas seulement à la racine de composition). Ce n'est pas la même chose que Service Facade (un objet qui regroupe un petit ensemble connu de dépendances) ou Abstract Factory (un objet qui crée des instances d'un seul type - le type d'une Abstract Factory peut être générique mais la méthode ne l'est pas) .la source
MyControllerBase
n'est ni couplé à un conteneur DI spécifique, ni "vraiment" un exemple de l'anti-modèle Service Locator.GetService<T>
méthode générique . La résolution des dépendances arbitraires est l'odeur réelle, qui est présente et correcte dans l'exemple de l'OP.GetService<T>()
méthode permet de demander des classes arbitraires: "Ce que cette méthode fait, c'est regarder le conteneur DI actuel de l'application et tente de résoudre la dépendance. Assez standard je pense." . Je répondais à votre commentaire en haut de cette réponse. Ceci est 100% un localisateur de servicesLes meilleurs arguments contre l'anti-modèle Service Locator sont clairement énoncés par Mark Seemann, donc je ne vais pas trop expliquer pourquoi c'est une mauvaise idée - c'est un voyage d'apprentissage que vous devez prendre le temps de comprendre par vous-même (je recommande également le livre de Mark ).
OK donc pour répondre à la question - reformulons votre problème réel :
Il y a une question qui résout ce problème sur StackOverflow . Comme mentionné dans l'un des commentaires:
Vous cherchez au mauvais endroit la solution à votre problème. Il est important de savoir quand une classe en fait trop. Dans votre cas, je soupçonne fortement qu'il n'y a pas besoin d'un "contrôleur de base". En fait, dans la POO, il y a presque toujours pas besoin d'héritage du tout . Les variations de comportement et de fonctionnalité partagée peuvent être entièrement obtenues grâce à une utilisation appropriée des interfaces, ce qui se traduit généralement par un code mieux factorisé et encapsulé - et pas besoin de transmettre des dépendances aux constructeurs de superclasses.
Dans tous les projets sur lesquels j'ai travaillé où il y a un contrôleur de base, cela a été fait uniquement dans le but de partager des propriétés et des méthodes pratiques, telles que
IsUserLoggedIn()
etGetCurrentUserId()
. ARRÊTEZ . C'est une horrible mauvaise utilisation de l'héritage. Au lieu de cela, créez un composant qui expose ces méthodes et prenez une dépendance là où vous en avez besoin. De cette façon, vos composants resteront testables et leurs dépendances seront évidentes.Mis à part toute autre chose, lors de l'utilisation du modèle MVC, je recommanderais toujours des contrôleurs maigres . Vous pouvez en savoir plus à ce sujet ici, mais l'essence du modèle est simple, les contrôleurs dans MVC ne devraient faire qu'une seule chose: gérer les arguments passés par le framework MVC, déléguer d'autres préoccupations à un autre composant. Il s'agit là encore du principe de responsabilité unique à l'œuvre.
Il serait vraiment utile de connaître votre cas d'utilisation pour porter un jugement plus précis, mais honnêtement, je ne peux penser à aucun scénario où une classe de base est préférable à des dépendances bien factorisées.
la source
protected
des délégations unilignes vers les nouveaux composants. Ensuite, vous pouvez perdre BaseController et remplacer cesprotected
méthodes par des appels directs aux dépendances - cela simplifiera votre modèle et rendra toutes vos dépendances explicites (ce qui est une très bonne chose dans un projet avec des centaines de contrôleurs!)J'ajoute une réponse à cela sur la base des contributions de tous les autres. Merci beaucoup à tous. Voici d'abord ma réponse: "Non, il n'y a rien de mal à cela".
Réponse de Doc Brown "Service Facade" J'ai accepté cette réponse parce que ce que je cherchais (si la réponse était "non") était quelques exemples ou une extension de ce que je faisais. Il a fourni ceci en suggérant que A) il a un nom, et B) il y a probablement de meilleures façons de le faire.
Réponse de Benjamin Hodgson «Service Locator» Autant que j'apprécie les connaissances que j'ai acquises ici, ce que j'ai n'est pas un «Service Locator». Il s'agit d'une "façade de service". Tout dans cette réponse est correct mais pas pour mes circonstances.
Réponses USR
Je vais aborder cela plus en détail:
Je ne perds aucun outillage et je ne perds pas de frappe "statique". La façade de service retournera ce que j'ai configuré dans mon conteneur DI, ou
default(T)
. Et ce qu'il retourne est "tapé". La réflexion est encapsulée.Ce n'est certainement pas "rare". Comme j'utilise un contrôleur de base , chaque fois que je dois changer de constructeur, je devrai peut-être changer 10, 100, 1000 autres contrôleurs.
J'utilise l'injection de dépendance. C'est le but.
Et enfin, le commentaire de Jules sur la réponse de Benjamin, je ne perds aucune flexibilité. C'est ma façade de service. Je peux ajouter autant de paramètres
GetService<T>
que je veux pour distinguer les différentes implémentations, tout comme on le ferait lors de la configuration d'un conteneur DI. Ainsi, par exemple, je pourrais changerGetService<T>()
pourGetService<T>(string extraInfo = null)
contourner ce "problème potentiel".Quoi qu'il en soit, merci encore à tout le monde. Ça a été vraiment utile. À votre santé.
la source
GetService<T>()
méthode est capable de (tenter de) résoudre des dépendances arbitraires . Cela en fait un localisateur de services, pas une façade de service, comme je l'ai expliqué dans la note de bas de page de ma réponse. Si vous deviez le remplacer par un petit ensemble de méthodesGetServiceX()
/GetServiceY()
, comme le suggère @DocBrown, ce serait une façade.IContext
et injecter cela, et surtout: si vous ajoutez une nouvelle dépendance, le code sera toujours compilé - même si les tests échoueront au moment de l'exécution