L'injection de dépendance (ID) est un modèle bien connu et à la mode. La plupart des ingénieurs connaissent ses avantages, tels que:
- Rendre l'isolement dans les tests unitaires possible / facile
- Définir explicitement les dépendances d'une classe
- Faciliter une bonne conception ( principe de responsabilité unique (SRP), par exemple)
- Activer rapidement les implémentations de commutation (
DbLogger
au lieu de,ConsoleLogger
par exemple)
Je pense qu’il existe un consensus général dans l’industrie selon lequel l’ID constitue un bon modèle utile. Il n'y a pas trop de critiques pour le moment. Les inconvénients mentionnés dans la communauté sont généralement mineurs. Certains d'entre eux:
- Augmentation du nombre de cours
- Création d'interfaces inutiles
Actuellement, nous discutons de la conception d'architecture avec mon collègue. Il est plutôt conservateur, mais ouvert d'esprit. Il aime poser des questions, ce que j'estime satisfaisant, car de nombreux informaticiens se contentent de copier la dernière tendance, de répéter les avantages et, en général, de ne pas trop réfléchir - n'analysez pas trop en profondeur.
Les choses que je voudrais demander sont:
- Devrions-nous utiliser l'injection de dépendance quand nous n'avons qu'une seule implémentation?
- Devrions-nous interdire la création de nouveaux objets, à l’exception des objets langage / framework?
- L'injection d'une seule implémentation est-elle une mauvaise idée (disons que nous n'avons qu'une seule implémentation, nous ne voulons donc pas créer d'interface "vide") si nous n'envisageons pas de tester à l'unité une classe particulière?
UserService
cette classe, c'est juste un détenteur de logique. Une connexion à la base de données est injectée et des tests sont exécutés dans une transaction annulée. Beaucoup qualifieraient cette mauvaise pratique, mais j’ai trouvé que cela fonctionnait extrêmement bien. Il n'est pas nécessaire de contourner votre code uniquement pour effectuer des tests et vous obtenez le bogue qui permet de trouver le pouvoir des tests d'intégration.Réponses:
Premièrement, je voudrais séparer l’approche de conception du concept de cadre. L'injection de dépendance à son niveau le plus simple et le plus fondamental est simplement:
C'est ça. Notez que rien dans cela ne nécessite des interfaces, des frameworks, n'importe quel style d'injection, etc. Pour être juste, j'ai d'abord entendu parler de ce modèle il y a 20 ans. Ce n'est pas nouveau.
En raison de la confusion qui règne entre plus de 2 personnes dans le contexte injection de dépendance:
L'injection de dépendance est un modèle de composition d' objet .
Pourquoi des interfaces?
Les interfaces sont un contrat. Ils existent pour limiter le couplage étroit de deux objets. Toutes les dépendances n'ont pas besoin d'une interface, mais elles aident à l'écriture de code modulaire.
Lorsque vous ajoutez le concept de test unitaire, vous pouvez avoir deux implémentations conceptuelles pour une interface donnée: l'objet réel que vous souhaitez utiliser dans votre application et l'objet modulé que vous utilisez pour le code de test qui dépend de l'objet. Cela seul peut suffire à justifier l’interface.
Pourquoi des frameworks?
Initialiser et fournir des dépendances aux objets enfants peut être décourageant quand il y en a un grand nombre. Les cadres offrent les avantages suivants:
Ils présentent également les inconvénients suivants:
Cela dit, il y a des compromis à faire. Pour les petits projets où il n'y a pas beaucoup de pièces mobiles, et il n'y a pas de raison d'utiliser un cadre DI. Toutefois, pour les projets plus complexes où certains composants sont déjà conçus, le cadre peut être justifié.
Qu'en est-il de [article aléatoire sur Internet]?
Qu'en est-il? Souvent, les gens peuvent devenir trop zélés et ajouter un tas de restrictions et vous gronder si vous ne faites pas les choses de la "seule façon". Il n'y a pas une seule vraie façon. Voyez si vous pouvez extraire quelque chose d'utile de l'article et ignorer les choses avec lesquelles vous n'êtes pas d'accord.
En bref, réfléchissez par vous-même et essayez des solutions.
Travailler avec de "vieilles têtes"
Apprenez autant que vous le pouvez. Ce que vous constaterez avec beaucoup de développeurs qui travaillent dans leurs 70 ans, c'est qu'ils ont appris à ne pas être dogmatique sur beaucoup de choses. Ils ont des méthodes avec lesquelles ils travaillent depuis des décennies qui produisent des résultats corrects.
J'ai eu le privilège de travailler avec quelques-uns d'entre eux et ils peuvent fournir des commentaires brutalement honnêtes qui ont beaucoup de sens. Et là où ils voient de la valeur, ils ajoutent ces outils à leur répertoire.
la source
L'injection de dépendance est, comme la plupart des modèles, une solution aux problèmes . Commencez donc par vous demander si vous avez le problème en premier lieu. Sinon, en utilisant le modèle le plus probable fera du code pire .
Considérez d’abord si vous pouvez réduire ou éliminer les dépendances. Toutes choses étant égales par ailleurs, nous souhaitons que chaque composant d’un système ait le moins de dépendances possible. Et si les dépendances ont disparu, la question de l'injection ou non devient sans objet!
Prenons un module qui télécharge des données d’un service externe, les analyse, effectue des analyses complexes et écrit dans un fichier.
Désormais, si la dépendance au service externe est codée en dur, il sera vraiment difficile de tester le traitement interne de ce module. Vous pouvez donc décider d’injecter le service externe et le système de fichiers en tant que dépendances d’interface, ce qui vous permettra d’injecter des mocks, ce qui rendra possible le test unitaire de la logique interne.
Mais une solution bien meilleure consiste simplement à séparer l'analyse de l'input / output. Si l'analyse est extraite dans un module sans effets secondaires, il sera beaucoup plus facile à tester. Notez que se moquer est une odeur de code - ce n'est pas toujours évitable, mais en général, il est préférable de pouvoir tester sans se moquer. Donc, en éliminant les dépendances, vous évitez les problèmes que l'ID est censé résoudre. Notez qu'une telle conception adhère également beaucoup mieux à SRP.
Je tiens à souligner que l’ID ne facilite pas nécessairement la SRP ou d’autres bons principes de conception, tels que la séparation des préoccupations, une forte cohésion / faible couplage, etc. Cela pourrait aussi bien avoir l’effet inverse. Considérons une classe A qui utilise une autre classe B en interne. B n’est utilisé que par A et est donc entièrement encapsulé et peut être considéré comme un détail de mise en œuvre. Si vous modifiez ceci pour injecter B dans le constructeur de A, vous avez exposé les détails de cette implémentation et, maintenant, des connaissances sur cette dépendance et sur la façon d'initialiser B, la durée de vie de B, etc., doit exister séparément à un autre endroit du système. de A. Vous avez donc une architecture généralement pire avec des problèmes de fuite.
D'autre part, dans certains cas, l'ID est vraiment utile. Par exemple, pour des services globaux ayant des effets secondaires tels qu'un enregistreur.
Le problème vient du moment où les modèles et les architectures deviennent des objectifs en eux-mêmes plutôt que des outils. Il suffit de demander "Devrions-nous utiliser DI?" est un peu comme mettre la charrue avant le cheval. Vous devriez demander: "Avons-nous un problème?" et "Quelle est la meilleure solution à ce problème?"
Une partie de votre question se résume à: "Faut-il créer des interfaces superflues pour satisfaire les exigences du modèle?" Vous avez probablement déjà compris la réponse à cette question - absolument pas ! Quiconque vous dit le contraire essaie de vous vendre quelque chose - des heures de consultation souvent très coûteuses. Une interface n'a de valeur que si elle représente une abstraction. Une interface qui imite simplement la surface d'une classe unique est appelée "interface d'en-tête" et c'est un anti-modèle connu.
la source
A
utilisationsB
en production ont été testéesMockB
, nos tests ne nous indiquent pas si cela fonctionnera en production. Lorsque des composants purs (sans effets secondaires) d'un modèle de domaine s'injectent et se moquent, le résultat est un énorme gaspillage de temps pour tout le monde, une base de code gonflée et fragile et une faible confiance en le système résultant. Mock aux frontières du système, pas entre des morceaux arbitraires du même système.D'après mon expérience, l'injection de dépendance présente plusieurs inconvénients.
Tout d'abord, l'utilisation de DI ne simplifie pas les tests automatisés autant que ceux annoncés. L'unité de test d'une classe avec une implémentation fictive d'une interface vous permet de valider comment cette classe va interagir avec l'interface. Autrement dit, cela vous permet de tester comment la classe sous test utilise le contrat fourni par l'interface. Cependant, cela donne une bien plus grande assurance que l'entrée de la classe sous test dans l'interface est comme prévu. Cela donne une assurance plutôt médiocre que la classe sous test répond comme prévu à la sortie de l'interface car il s'agit d'une sortie simulée presque universellement, elle-même sujette à des bugs, à des simplifications excessives, etc. En bref, cela ne vous permet PAS de valider que la classe se comportera comme prévu avec une implémentation réelle de l'interface.
Deuxièmement, DI rend la navigation dans le code beaucoup plus difficile. Lorsque vous essayez de naviguer vers la définition des classes utilisées en tant qu'entrée de fonctions, une interface peut être un désagrément mineur (par exemple, lorsqu'il n'y a qu'une seule implémentation) ou une perte de temps majeure (par exemple, lorsqu'une interface trop générique, telle qu'IDisposable, est utilisée) en essayant de trouver l'implémentation réelle utilisée. Cela peut transformer un exercice simple du type "J'ai besoin de corriger une exception de référence null dans le code qui se produit juste après que cette instruction de journalisation est imprimée" en un effort d'une journée.
Troisièmement, l'utilisation de DI et de cadres est une arme à double tranchant. Cela peut grandement réduire la quantité de code de plaque de chaudière nécessaire aux opérations courantes. Toutefois, cela nécessite au minimum une connaissance détaillée du cadre ID spécifique pour comprendre comment ces opérations courantes sont réellement connectées. Pour comprendre comment les dépendances sont chargées dans le framework et ajouter une nouvelle dépendance dans le framework à injecter, vous devrez peut-être lire une assez grande quantité de documentation et suivre quelques tutoriels de base sur le framework. Cela peut transformer certaines tâches simples en tâches prenant beaucoup de temps.
la source
J'ai suivi le conseil de Mark Seemann de "Dependency injection in .NET" - à supposer.
DI devrait être utilisé lorsque vous avez une «dépendance volatile», par exemple, il y a une chance raisonnable que cela change.
Donc, si vous pensez avoir plus d'une implémentation dans le futur ou si la mise en œuvre peut changer, utilisez DI. Sinon
new
c'est bien.la source
Ma plus grande bête noire à propos de DI a déjà été mentionnée dans quelques réponses, mais je vais en dire un peu plus ici. DI (comme c'est le cas la plupart du temps aujourd'hui, avec les conteneurs, etc.) vraiment, nuit vraiment à la lisibilité du code. Et la lisibilité du code est sans doute la raison derrière la plupart des innovations de programmation actuelles. Comme quelqu'un l'a dit - écrire du code est facile. La lecture du code est difficile. Mais c’est aussi extrêmement important, à moins que vous n’écriviez une sorte de petit utilitaire jetable une seule fois.
Le problème avec DI à cet égard est qu’il est opaque. Le conteneur est une boîte noire. Les objets apparaissent simplement de quelque part et vous n'avez aucune idée - qui les a construits et quand? Qu'est-ce qui a été transmis au constructeur? Avec qui je partage cette instance? Qui sait...
Et lorsque vous travaillez principalement avec des interfaces, toutes les fonctionnalités "aller à la définition" de votre IDE partent en fumée. Il est extrêmement difficile de retracer le flux du programme sans l'exécuter et se contenter de voir exactement quelle implémentation de l'interface a été utilisée à CET emplacement particulier. Et parfois, des obstacles techniques vous empêchent de franchir le pas. Et même si vous le pouvez, si cela implique de traverser les entrailles tordues du conteneur d’ID, l’affaire devient rapidement un exercice frustrant.
Pour travailler efficacement avec un morceau de code qui utilise DI, vous devez le connaître et déjà savoir ce qui se passe où.
la source
Bien que l’ID en général soit sûrement une bonne chose, je suggérerais de ne pas l’utiliser aveuglément pour tout. Par exemple, je n’injecte jamais de bûcheron. Un des avantages de DI est de rendre les dépendances explicites et claires. Il est inutile d’énumérer
ILogger
la dépendance de presque toutes les classes, c’est un fouillis. Il est de la responsabilité de l'enregistreur de vous fournir la flexibilité dont vous avez besoin. Tous mes enregistreurs sont des membres finaux statiques, je peux envisager d’injecter un enregistreur lorsque j’ai besoin d’un enregistreur non statique.Ceci est un inconvénient du cadre DI donné ou du cadre moqueur, et non du DI lui-même. Dans la plupart des endroits, mes cours dépendent de cours concrets, ce qui signifie qu’il n’ya pas de passe-partout. Guice (un framework Java DI) lie par défaut une classe à elle-même et je n'ai qu'à remplacer la liaison dans les tests (ou les câbler manuellement à la place).
Je crée uniquement les interfaces en cas de besoin (ce qui est plutôt rare). Cela signifie que parfois, je dois remplacer toutes les occurrences d'une classe par une interface, mais l'EDI peut le faire pour moi.
Oui, mais évitez d'ajouter un passe-partout .
Non. Il y aura de nombreuses classes value (immuable) et data (mutable), où les instances sont simplement créées et transmises et où il est inutile de les injecter, car elles ne sont jamais stockées dans un autre objet (ou uniquement dans un autre). ces objets).
Pour eux, vous devrez peut-être injecter une usine, mais la plupart du temps cela n’a aucun sens (imaginez, par exemple,
@Value class NamedUrl {private final String name; private final URL url;}
vous n’avez vraiment pas besoin d’une usine ici et il n’ya rien à injecter).IMHO c'est bien tant que cela ne provoque pas de gonflement du code. N'injectez pas la dépendance mais ne créez pas l'interface (et pas de XML de configuration fou!), Car vous pourrez le faire plus tard sans problème.
En fait, dans mon projet actuel, il existe quatre classes (sur des centaines), que j'ai décidé d' exclure de DI car ce sont des classes simples utilisées dans trop d'endroits, y compris des objets de données.
La surcharge d'exécution est un autre inconvénient de la plupart des structures DI. Cela peut être déplacé pour la compilation (pour Java, il y a Dagger , aucune idée des autres langages).
Un autre inconvénient est la magie qui se produit partout, qui peut être atténuée (par exemple, j’ai désactivé la création de proxy lors de l’utilisation de Guice).
la source
Je dois dire qu'à mon avis, la notion d'injection de dépendance est surestimée.
DI est l'équivalent moderne des valeurs globales. Les éléments que vous injectez sont des singletons globaux et des objets de code purs. Sinon, vous ne pourriez pas les injecter. La plupart des utilisations de DI vous sont imposées pour utiliser une bibliothèque donnée (JPA, Spring Data, etc.). Dans la plupart des cas, le DI fournit l’environnement idéal pour nourrir et cultiver les spaghettis.
En toute honnêteté, le moyen le plus simple de tester une classe est de s'assurer que toutes les dépendances sont créées dans une méthode qui peut être remplacée. Créez ensuite une classe Test dérivée de la classe réelle et substituez cette méthode.
Ensuite, vous instanciez la classe Test et testez toutes ses méthodes. Cela ne sera pas clair pour certains d'entre vous - les méthodes que vous testez appartiennent à la classe sous test. Et tous ces tests de méthodes ont lieu dans un fichier de classe unique, la classe de tests unitaires associée à la classe en cours de test. Il n'y a pas de frais généraux ici - c'est ainsi que fonctionnent les tests unitaires.
En code, ce concept ressemble à ceci ...
Le résultat est exactement équivalent à l'utilisation de DI: le ClassUnderTest est configuré pour les tests.
Les seules différences sont que ce code est parfaitement concis, complètement encapsulé, plus facile à coder, plus facile à comprendre, plus rapide, utilise moins de mémoire, ne nécessite pas de configuration alternative, ne nécessite pas de framework, ne sera jamais la cause de 4 pages (WTF!) Trace de pile qui inclut exactement ZERO (0) classes que vous avez écrites et qui est tout à fait évident pour quiconque possède la moindre connaissance en langage d’utilisation, du débutant au gourou (on pourrait penser, mais se tromper).
Cela étant dit, bien sûr, nous ne pouvons pas l'utiliser - c'est trop évident et pas assez tendance.
À la fin de la journée, cependant, ma plus grande préoccupation avec DI est celle des projets que j’ai vu échouer lamentablement. Ce sont tous des bases de code gigantesques où DI était le ciment qui a tout réuni. DI n’est pas une architecture - elle n’est utile que dans une poignée de situations, qui vous sont généralement imposées pour utiliser une autre bibliothèque (JPA, Spring Data, etc.). Généralement, dans une base de code bien conçue, la plupart des utilisations de DI se produiraient à un niveau inférieur à celui de vos activités de développement quotidiennes.
la source
bar
ne peuvent être appelées qu'aprèsfoo
, c'est une mauvaise API. Il prétend fournir des fonctionnalités (bar
), mais nous ne pouvons pas les utiliser (car ellesfoo
n'auraient peut-être pas été appelées). Si vous souhaitez conserver votreinitializeDependencies
modèle (anti?), Vous devez au moins le rendre privé / protégé et l'appeler automatiquement à partir du constructeur, pour que l'API soit sincère.En réalité, votre question se résume à la question suivante: "Le test unitaire est-il mauvais?"
99% de vos classes de substitution à injecter seront des simulacres permettant les tests unitaires.
Si vous effectuez des tests unitaires sans DI, vous avez des difficultés à obtenir que les classes utilisent des données fictives ou un service fictif. Disons «partie de la logique», car vous n’êtes peut-être pas séparé en services.
Il existe d'autres moyens de le faire, mais l'ID est bon et flexible. Une fois que vous avez le temps de le tester, vous êtes pratiquement obligé de l'utiliser partout, car vous avez besoin d'un autre élément de code, même s'il s'agit de la soi-disant «DI du pauvre» qui instancie les types concrets.
Il est difficile d'imaginer un inconvénient si grave que l'avantage des tests unitaires est dépassé.
la source
new
méthode codée en dur dans une méthode, nous savons que le même code exécuté en production a été exécuté pendant les tests.Order
exemple est un test unitaire: il teste une méthode (latotal
méthode deOrder
). Vous vous plaignez qu'il appelle le code de 2 classes, ma réponse est donc quoi ? Nous ne testons pas "2 classes à la fois", nous testons latotal
méthode. Nous ne devrions pas nous soucier de la façon dont une méthode fait son travail: ce sont des détails de mise en œuvre; les tester provoque une fragilité, un couplage étroit, etc. Nous nous soucions seulement du comportement de la méthode (valeur de retour et effets secondaires), pas des classes / modules / registres de la CPU / etc. il a utilisé dans le processus.