Depuis que j'ai appris (et aimé) les tests automatisés, je me suis retrouvé à utiliser le modèle d'injection de dépendance dans presque tous les projets. Est-il toujours approprié d'utiliser ce modèle lorsque vous travaillez avec des tests automatisés? Y a-t-il des situations dans lesquelles vous devriez éviter d'utiliser l'injection de dépendance?
design-patterns
dependency-injection
Tom Squires
la source
la source
Réponses:
Fondamentalement, l'injection de dépendance émet certaines hypothèses (généralement, mais pas toujours valables) sur la nature de vos objets. Si ceux-ci sont faux, DI pourrait ne pas être la meilleure solution:
Tout d’abord, essentiellement, DI suppose que le couplage étroit d’implémentations d’objets est TOUJOURS mauvais . C'est l'essence du principe d'inversion de dépendance: "une dépendance ne devrait jamais être faite sur une concrétion; seulement sur une abstraction".
Ceci ferme l'objet dépendant à modifier en fonction d'une modification de l'implémentation concrète; une classe dépendant de ConsoleWriter devra être modifiée si la sortie doit plutôt être placée dans un fichier, mais si la classe dépendait uniquement d’un IWriter exposant une méthode Write (), nous pouvons remplacer le ConsoleWriter actuellement utilisé par un FileWriter. la classe dépendante ne connaîtrait pas la différence (principe de substitution de Liskhov).
Cependant, une conception ne peut JAMAIS être fermée à tous les types de changement; si la conception de l'interface IWriter elle-même change, pour ajouter un paramètre à Write (), vous devez maintenant modifier un objet de code supplémentaire (l'interface IWriter), au-dessus de l'objet / de la méthode d'implémentation et de ses utilisations. Si des modifications de l'interface réelle sont plus susceptibles que des modifications de la mise en œuvre de cette interface, un couplage faible (et des dépendances entre coupleurs DI) peut entraîner plus de problèmes qu'il n'en résout.
Deuxièmement, et corollaire, DI suppose que la classe dépendante n’est JAMAIS un bon endroit pour créer une dépendance . Cela va au principe de responsabilité unique; si vous avez un code qui crée une dépendance et qui l'utilise également, la classe dépendante peut être amenée à changer (modification de l'utilisation OU de l'implémentation) pour deux raisons, violant SRP.
Cependant, encore une fois, l’ajout de couches d’indirection pour DI peut être une solution à un problème qui n’existe pas; s'il est logique d'encapsuler la logique dans une dépendance, mais que cette logique est la seule implémentation de cette dépendance, il est plus pénible de coder la résolution à couplage lâche de la dépendance (injection, emplacement du service, usine) qu'elle ne le serait juste utiliser
new
et oublier.Enfin, DI centralise par nature la connaissance de toutes les dépendances ET de leurs implémentations . Cela augmente le nombre de références que l'assemblage qui effectue l'injection doit avoir et, dans la plupart des cas, ne réduit PAS le nombre de références requis par les assemblys de classes dépendantes réelles.
QUELQUE CHOSE, QUELQUE PART, doit avoir une connaissance de la dépendance, de l’interface de dépendance et de son implémentation pour pouvoir "relier les points" et satisfaire cette dépendance. DI tend à placer toutes ces connaissances à un niveau très élevé, soit dans un conteneur IoC, soit dans le code qui crée des objets "principaux" tels que la fiche principale ou le contrôleur qui doit hydrater (ou fournir des méthodes d'usine) les dépendances. Cela peut mettre beaucoup de code nécessairement étroitement lié et beaucoup de références d'assemblage à des niveaux élevés de votre application, qui n'a besoin que de cette connaissance pour le "masquer" des classes dépendantes réelles (qui d'un point de vue très fondamental est la meilleur endroit pour avoir cette connaissance; où il est utilisé).
De plus, normalement, il ne supprime pas lesdites références de bas en bas dans le code; une personne à charge doit toujours référencer la bibliothèque contenant l'interface pour sa dépendance, qui se trouve dans l'un des trois emplacements suivants:
Tout cela, encore une fois, pour résoudre un problème là où il n’en existe peut-être pas.
la source
En dehors des infrastructures d'injection de dépendance, l'injection de dépendance (via l'injection du constructeur ou l'injection du setter) est presque un jeu à somme nulle: vous diminuez le couplage entre l'objet A et sa dépendance B, mais maintenant tout objet ayant besoin d'une instance de A doit maintenant construit également l'objet B.
Vous avez légèrement réduit le couplage entre A et B, mais réduit l'encapsulation de A et augmenté le couplage entre A et toute classe qui doit construire une instance de A, en les couplant également aux dépendances de A.
Ainsi, l’injection de dépendance (sans cadre) est à la fois nuisible et utile.
Toutefois, le coût supplémentaire est souvent facilement justifiable: si le code client en sait plus sur la manière de construire la dépendance que l’objet lui-même, l’injection de dépendance réduit réellement le couplage; Par exemple, un scanner ne sait pas comment obtenir ou créer un flux d'entrée pour analyser une entrée, ni quelle source le code client veut analyser, de sorte que l'injection de constructeur d'un flux d'entrée est la solution évidente.
Le test est une autre justification, afin de pouvoir utiliser des dépendances factices. Cela devrait impliquer l’ajout d’un constructeur supplémentaire utilisé à des fins de test et permettant d’injecter des dépendances: si vous modifiez vos constructeurs de manière à toujours demander l’injection de dépendances, vous devez connaître les dépendances de ces dépendances afin de construire votre dépendances directes, et vous ne pouvez pas faire le travail.
Cela peut être utile, mais vous devez absolument vous demander pour chaque dépendance, l’avantage des tests en vaut-il la peine, et vais-je vraiment vouloir me moquer de cette dépendance pendant les tests?
Lorsqu'un cadre d'injection de dépendance est ajouté et que la construction des dépendances est déléguée non pas au code client mais au cadre, l'analyse coûts / avantages change considérablement.
Dans un cadre d'injection de dépendance, les compromis sont un peu différents; ce que vous perdez en injectant une dépendance, c’est la capacité de savoir facilement sur quelle implémentation vous vous fiez et de transférer la responsabilité de décider de la dépendance sur laquelle vous comptez compter en un processus de résolution automatisé (par exemple, si nous avons besoin d’un @ Inject'ed Foo , il doit y avoir quelque chose qui @Provides Foo, et dont les dépendances injectées sont disponibles), ou à un fichier de configuration de haut niveau qui spécifie quel fournisseur doit être utilisé pour chaque ressource, ou à un hybride des deux (par exemple, processus de résolution automatisé des dépendances pouvant être remplacées, le cas échéant, à l'aide d'un fichier de configuration).
Comme pour l’injection de constructeur, je pense que l’avantage de le faire finit par avoir un coût très similaire: vous n’avez pas besoin de savoir qui fournit les données sur lesquelles vous comptez et, s’il existe plusieurs potentiels. fournisseurs, vous n’avez pas besoin de connaître l’ordre privilégié pour vérifier la présence des fournisseurs, assurez-vous que chaque emplacement nécessitant des contrôles de données pour tous les fournisseurs potentiels, etc., car tout cela est géré à un niveau élevé par l’injection de dépendance Plate-forme.
Bien que je n’ai pas personnellement beaucoup d’expérience avec les frameworks DI, j’ai l’impression que ceux-ci offrent plus d’avantages que de coûts lorsque trouver le bon fournisseur des données ou du service dont vous avez besoin a un coût supérieur à celui des maux de tête, quand quelque chose échoue, de ne pas savoir immédiatement localement quel code a fourni les données incorrectes qui ont causé une défaillance ultérieure de votre code.
Dans certains cas, d’autres modèles qui masquent les dépendances (par exemple, les localisateurs de service) avaient déjà été adoptés (et ont peut-être également fait leurs preuves) lorsque les cadres d’assurance DI sont entrés en scène, et les cadres d’ID ont été adoptés car ils offraient un avantage concurrentiel, par exemple moins de code passe-partout, ou potentiellement moins pour masquer le fournisseur de dépendance lorsqu'il devient nécessaire de déterminer quel fournisseur est réellement utilisé.
la source
si vous créez des entités de base de données, vous devriez plutôt avoir une classe de fabrique que vous allez injecter à votre contrôleur,
si vous avez besoin de créer des objets primitifs tels que ints ou long. Vous devez également créer "à la main" la plupart des objets de la bibliothèque standard tels que les dates, les guides, etc.
si vous souhaitez injecter des chaînes de configuration, il est probablement préférable d’injecter des objets de configuration (en général, il est recommandé d’envelopper des types simples dans des objets significatifs: int temperatureInCelsiusDegrees -> CelciusDeegree temperature)
Et n'utilisez pas Service locator comme alternative à l'injection de dépendance, c'est un anti-motif, plus d'infos: http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx
la source
Lorsque vous ne gagnez rien en rendant votre projet maintenable et testable.
Sérieusement, j'adore IoC et DI en général, et je dirais que 98% du temps, j'utiliserai ce modèle sans faute. C'est particulièrement important dans un environnement multi-utilisateur, où votre code peut être réutilisé à plusieurs reprises par différents membres de l'équipe et différents projets, car il sépare la logique de la mise en œuvre. La journalisation en est un excellent exemple. Une interface ILog injectée dans une classe est mille fois plus facile à gérer que de simplement brancher votre framework de journalisation du jour, car vous n'avez aucune garantie qu'un autre projet utilisera le même framework de journalisation (s'il utilise un du tout!).
Cependant, il y a des moments où ce n'est pas un motif applicable. Par exemple, les points d'entrée fonctionnels implémentés dans un contexte statique par un initialiseur non remplaçable (WebMethods, je vous regarde, mais votre méthode Main () dans votre classe Program est un autre exemple) ne peuvent tout simplement pas avoir des dépendances injectées lors de l'initialisation. temps. J'irais aussi jusqu'à dire qu'un prototype, ou tout morceau de code d'enquête jetable, est également un mauvais candidat; Les avantages de DI sont plutôt des avantages à moyen et à long terme (testabilité et maintenabilité). Si vous êtes certain de jeter la majorité d'un code d'ici une semaine environ, je dirais que vous ne gagnez rien en isolant dépendances, passez simplement le temps que vous passeriez normalement à tester et à isoler les dépendances pour que le code fonctionne.
Dans l’ensemble, il est judicieux d’adopter une approche pragmatique à l’égard d’une méthodologie ou d’un modèle, car rien n’est applicable dans 100% des cas.
Une chose à noter est votre commentaire sur les tests automatisés: ma définition de ceci est des tests fonctionnels automatisés , par exemple des tests de sélénium scriptés si vous êtes dans un contexte Web. Ce sont généralement des tests complètement en boîte noire, sans qu'il soit nécessaire de connaître le fonctionnement interne du code. Si vous vous référiez aux tests unitaires ou d'intégration, je dirais que le modèle DI est presque toujours applicable à tout projet qui s'appuie fortement sur ce type de test en boîte blanche, car il vous permet par exemple de tester des éléments tels que méthodes qui touchent la base de données sans qu’il soit nécessaire qu’une base de données soit présente.
la source
Tandis que les autres réponses se concentrent sur les aspects techniques, je voudrais ajouter une dimension pratique.
Au fil des ans, j’ai conclu à la nécessité de satisfaire à plusieurs exigences pratiques pour pouvoir introduire l’injection de dépendance.
Il devrait y avoir une raison de l'introduire.
Cela semble évident, mais si votre code ne récupère que des éléments de la base de données et le renvoie sans aucune logique, l'ajout d'un conteneur DI rend les choses plus complexes sans aucun avantage réel. Les tests d'intégration seraient plus importants ici.
L'équipe doit être formée et à bord.
À moins que la majorité de l'équipe soit à bord et comprenne que DI ajoute, l'inversion du conteneur de contrôles devient un moyen supplémentaire de faire les choses et rend la base de code encore plus compliquée.
Si la DI est introduite par un nouveau membre de l'équipe, parce qu'il la comprend et l'aime bien et veut juste montrer qu'il est bon, ET que l'équipe n'est pas activement impliquée, il y a un risque réel que cela diminue la qualité de son travail. le code.
Vous devez tester
Bien que le découplage soit généralement une bonne chose, DI peut déplacer la résolution d'une dépendance du temps de compilation au temps d'exécution. C'est en fait assez dangereux si vous ne testez pas bien. Les échecs de résolution en temps d'exécution peuvent être coûteux à suivre et à résoudre.
(Il ressort clairement de votre test que vous le faites, mais de nombreuses équipes ne le testent pas dans la mesure requise par DI.)
la source
Ce n'est pas une réponse complète, mais juste un autre point.
Lorsque vous avez une application qui démarre une fois et s'exécute pendant une longue période (comme une application Web), DI peut être utile.
Lorsque vous avez une application qui démarre plusieurs fois et s'exécute plus rapidement (comme une application mobile), vous ne voulez probablement pas le conteneur.
la source
Essayez d'utiliser les principes de base de la programmation orientée objet: utilisez l' héritage pour extraire des fonctionnalités communes, encapsuler (cacher) des éléments devant être protégés du monde extérieur à l'aide de membres / types privés / internes / protégés. Utilisez n'importe quel framework de test puissant pour injecter du code pour les tests uniquement, par exemple https://www.typemock.com/ ou https://www.telerik.com/products/mocking.aspx .
Essayez ensuite de le réécrire avec DI et comparez le code que vous verrez habituellement avec DI:
D'après ce que j'ai vu, presque toujours, la qualité du code diminue avec DI.
Toutefois, si vous utilisez uniquement un modificateur d'accès "public" dans la déclaration de classe et / ou des modificateurs public / privé pour les membres, et / ou si vous n'avez pas le choix d'acheter des outils de test coûteux, vous aurez besoin d'un test unitaire qui peut " t être remplacés par des tests d’intégration, et / ou vous avez déjà des interfaces pour les classes que vous envisagez d’injecter, DI est un bon choix!
ps probablement j'aurai beaucoup de points négatifs pour cet article, je crois parce que la plupart des développeurs modernes ne comprennent tout simplement pas comment et pourquoi utiliser un mot clé interne et comment diminuer le couplage de vos composants et enfin pourquoi le diminuer) enfin, juste essayez de coder et de comparer
la source
Une alternative à l'injection de dépendance consiste à utiliser un localisateur de services . Un localisateur de service est plus facile à comprendre, à déboguer et facilite la construction d'un objet, en particulier si vous n'utilisez pas de structure DI. Les localisateurs de service constituent un bon modèle pour la gestion des dépendances statiques externes , par exemple une base de données que vous auriez sinon à passer dans chaque objet de votre couche d'accès aux données.
Lors de la refactorisation d'un code hérité , il est souvent plus facile de refactoriser un localisateur de services que l'injection de dépendance. Tout ce que vous faites est de remplacer les instanciations par des recherches de service, puis de simuler le service dans votre test unitaire.
Cependant, le localisateur de services présente certains inconvénients . Il est plus difficile de connaître les dépendances d'une classe, car celles-ci sont cachées dans l'implémentation de la classe, et non dans les constructeurs ou les setters. Et créer deux objets qui reposent sur différentes implémentations du même service est difficile, voire impossible.
TLDR : Si votre classe a des dépendances statiques ou si vous refactoring du code hérité, un localisateur de service est sans doute meilleur que DI.
la source