Contexte: j'utilise C #
J'ai conçu une classe, et afin de l'isoler, et de faciliter les tests unitaires, je passe dans toutes ses dépendances; il ne fait aucune instanciation d'objet en interne. Cependant, au lieu de référencer des interfaces pour obtenir les données dont il a besoin, je l'ai en référence à des fonctions générales renvoyant les données / comportements dont il a besoin. Lorsque j'injecte ses dépendances, je peux simplement le faire avec des expressions lambda.
Pour moi, cela semble être une meilleure approche parce que je n'ai pas à me moquer de moi pendant les tests unitaires. De plus, si l'implémentation environnante a des changements fondamentaux, je n'aurai besoin que de changer la classe d'usine; aucun changement dans la classe contenant la logique ne sera nécessaire.
Cependant, je n'ai jamais vu l'IoC faire de cette façon auparavant, ce qui me fait penser qu'il y a des pièges potentiels que je pourrais manquer. Le seul auquel je peux penser est une incompatibilité mineure avec une version antérieure de C # qui ne définit pas Func, et ce n'est pas un problème dans mon cas.
Existe-t-il des problèmes avec l'utilisation de délégués génériques / de fonctions d'ordre supérieur telles que Func pour IoC, par opposition à des interfaces plus spécifiques?
la source
Func
puisque vous pouvez nommer les paramètres, où vous pouvez indiquer leur intention.Réponses:
Si une interface ne contient qu'une seule fonction, pas plus, et qu'il n'y a aucune raison impérieuse d'introduire deux noms (le nom de l'interface et le nom de la fonction à l'intérieur de l'interface), l'utilisation d'un à la
Func
place peut éviter le code passe-partout inutile et est dans la plupart des cas préférable - tout comme lorsque vous commencez à concevoir un DTO et que vous reconnaissez qu'il n'a besoin que d'un seul attribut membre.Je suppose que beaucoup de gens sont plus habitués à utiliser
interfaces
, car au moment où l'injection de dépendances et l'IoC devenaient populaires, il n'y avait pas vraiment d'équivalent à laFunc
classe en Java ou C ++ (je ne suis même pas sûr si elleFunc
était disponible à ce moment-là en C # ). Donc, beaucoup de tutoriels, d'exemples ou de manuels préfèrent toujours leinterface
formulaire, même si l'utilisationFunc
serait plus élégante.Vous pouvez consulter mon ancienne réponse sur le principe de ségrégation d'interface et l'approche de conception de flux de Ralf Westphal. Ce paradigme implémente DI avec des
Func
paramètres exclusivement , pour exactement la même raison que vous avez déjà mentionnée par vous-même (et quelques autres). Donc, comme vous le voyez, votre idée n'est pas vraiment nouvelle, bien au contraire.Et oui, j'ai utilisé cette approche par moi-même, pour le code de production, pour les programmes qui devaient traiter des données sous la forme d'un pipeline avec plusieurs étapes intermédiaires, y compris des tests unitaires pour chacune des étapes. Je peux donc vous donner une expérience de première main qui peut très bien fonctionner.
la source
L'un des principaux avantages que je trouve avec l'IoC est qu'il me permettra de nommer toutes mes dépendances en nommant leur interface, et le conteneur saura lequel fournir au constructeur en faisant correspondre les noms de type. C'est pratique et cela permet des noms de dépendances beaucoup plus descriptifs que
Func<string, string>
.Je trouve également souvent que même avec une seule dépendance simple, il doit parfois avoir plus d'une fonction - une interface vous permet de regrouper ces fonctions de manière auto-documentée, par opposition à avoir plusieurs paramètres qui se lisent tous comme
Func<string, string>
,Func<string, int>
.Il y a certainement des moments où il est utile d'avoir simplement un délégué qui est passé en tant que dépendance. C'est une question de jugement quant à l'utilisation d'un délégué par rapport à une interface avec très peu de membres. À moins qu'il ne soit vraiment clair quel est le but de l'argument, je me trompe généralement du côté de la production de code auto-documenté; c'est à dire. écrire une interface.
la source
Pas vraiment. Un Func est son propre type d'interface (au sens anglais, pas au sens C #). "Ce paramètre est quelque chose qui fournit X à la demande." Le Func a même l'avantage de fournir paresseusement les informations uniquement selon les besoins. Je le fais un peu et le recommande avec modération.
Quant aux inconvénients:
T
et d'autres le sontFunc<T>
.la source
Prenons un exemple simple - vous injectez peut-être un moyen de journalisation.
Injecter une classe
Je pense que c'est assez clair ce qui se passe. De plus, si vous utilisez un conteneur IoC, vous n'avez même pas besoin d'injecter quoi que ce soit explicitement, vous ajoutez simplement à votre racine de composition:
Lors du débogage
Worker
, un développeur n'a qu'à consulter la racine de la composition pour déterminer quelle classe concrète est utilisée.Si un développeur a besoin d'une logique plus compliquée, il a toute l'interface avec laquelle travailler:
Injection d'une méthode
Tout d' abord, notez que le paramètre constructeur a un nom plus maintenant,
methodThatLogs
. Ceci est nécessaire car vous ne pouvez pas dire ce que vous êtesAction<string>
censé faire. Avec l'interface, c'était complètement clair, mais ici, nous devons recourir à la dénomination des paramètres. Cela semble intrinsèquement moins fiable et plus difficile à appliquer lors d'une génération.Maintenant, comment injectons-nous cette méthode? Eh bien, le conteneur IoC ne le fera pas pour vous. Il vous reste donc à l'injecter explicitement lorsque vous instanciez
Worker
. Cela soulève quelques problèmes:Worker
Worker
trouveront qu'il est plus difficile de déterminer quelle instance concrète est appelée. Ils ne peuvent pas simplement consulter la racine de la composition; ils devront tracer à travers le code.Et si nous avions besoin d'une logique plus compliquée? Votre technique n'expose qu'une seule méthode. Maintenant, je suppose que vous pouvez faire cuire les choses compliquées dans le lambda:
mais quand vous écrivez vos tests unitaires, comment testez-vous cette expression lambda? Il est anonyme, donc votre framework de test unitaire ne peut pas l'instancier directement. Peut-être que vous pouvez trouver un moyen intelligent de le faire, mais ce sera probablement un PITA plus grand que d'utiliser une interface.
Résumé des différences:
Si vous êtes d'accord avec tout ce qui précède, vous pouvez injecter uniquement la méthode. Sinon, je vous suggère de vous en tenir à la tradition et d'injecter une interface.
la source
Worker
dépendance viable à la fois, permettant à chaque lambda d'être injecté indépendamment jusqu'à ce que toutes les dépendances soient satisfaites. Ceci est similaire à une application partielle dans FP. De plus, l'utilisation de lambdas permet d'éliminer l'état implicite et le couplage caché entre les dépendances.Considérez le code suivant que j'ai écrit il y a longtemps:
Il prend un emplacement relatif dans un fichier modèle, le charge en mémoire, rend le corps du message et assemble l'objet de messagerie.
Vous pourriez regarder
IPhysicalPathMapper
et penser: "Il n'y a qu'une seule fonction. Cela pourrait être unFunc
." Mais en réalité, le problème ici est que celaIPhysicalPathMapper
ne devrait même pas exister. Une bien meilleure solution consiste à simplement paramétrer le chemin :Cela soulève une foule d'autres questions pour améliorer ce code. Par exemple, peut-être qu'il devrait simplement accepter un
EmailTemplate
, puis peut-être qu'il devrait accepter un modèle pré-rendu, et alors peut-être qu'il devrait simplement être aligné.C'est pourquoi je n'aime pas l'inversion du contrôle comme modèle omniprésent. Il est généralement considéré comme cette solution divine pour composer tout votre code. Mais en réalité, si vous l'utilisez de manière omniprésente (plutôt que parcimonieuse), cela aggrave votre code en vous encourageant à introduire de nombreuses interfaces inutiles qui sont utilisées complètement à l'envers. (En arrière dans le sens où l'appelant devrait vraiment être responsable de l'évaluation de ces dépendances et de la transmission du résultat plutôt que de la classe elle-même appelant l'appel.)
Les interfaces doivent être utilisées avec parcimonie, et l'inversion du contrôle et l'injection de dépendance doivent également être utilisées avec parcimonie. Si vous en avez de grandes quantités, votre code devient beaucoup plus difficile à déchiffrer.
la source