Quand n'est-il pas approprié d'utiliser le modèle d'injection de dépendance?

124

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?

Tom Squires
la source
1
Related (probablement pas en double) - stackoverflow.com/questions/2407540/…
Steve314 le
3
Cela ressemble un peu à l'anti-motif Golden Hammer. en.wikipedia.org/wiki/Anti-pattern
Cuga

Réponses:

123

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 newet 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:

    • le tout dans un seul assemblage "d'interfaces" qui devient très centré sur l'application,
    • chacune à côté de la ou des implémentations principales, ce qui élimine l'avantage de ne pas avoir à recompiler les dépendants lorsque les dépendances changent, ou
    • une ou deux pièces dans des assemblages hautement cohésifs, ce qui gonfle le nombre d'assemblages, augmente considérablement les temps de "construction complète" et diminue les performances d'application.

    Tout cela, encore une fois, pour résoudre un problème là où il n’en existe peut-être pas.

KeithS
la source
1
PÉR = principe de responsabilité unique, pour qui se pose la question.
Theodore Murdock
1
Oui, et LSP est le principe de substitution de Liskov; étant donné une dépendance à A, cette dépendance devrait pouvoir être satisfaite par B qui dérive de A sans autre changement.
KeithS
l'injection de dépendance aide en particulier lorsque vous avez besoin d'obtenir la classe A de la classe B, de la classe D, etc. Dans ce cas (et uniquement dans ce cas), le cadre DI peut générer moins de déformation en assemblage que l'injection de pauvre. De plus, je n’ai jamais eu un goulet d’étranglement causé par DI .. pense aux coûts de maintenance, jamais aux coûts de processeur, car le code peut toujours être optimisé, mais le faire sans raison a un coût
GameDeveloper
Une autre hypothèse serait-elle "si une dépendance change, elle change partout"? Sinon, vous devez quand même regarder tous les cours de consommation
Richard Tingle
3
Cette réponse n'aborde pas l'impact de la testabilité des dépendances d'injection par rapport à leur codage en dur. Voir aussi le principe des dépendances explicites ( deviq.com/explicit-dependencies-principle )
ssmith
30

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é.

Theodore Murdock
la source
6
Juste un petit commentaire sur la dépendance moqueuse dans les tests et "vous devez connaître les dépendances de vos dépendances" ... Ce n'est pas vrai du tout. Il n'est pas nécessaire de connaître ou de se soucier de certaines dépendances de la mise en œuvre concrète si une maquette est injectée. Il suffit de connaître les dépendances directes de la classe sous test, qui sont satisfaites par les modèles.
Eric King
6
Vous comprenez mal, je ne parle pas lorsque vous injectez une maquette, je parle du code réel. Considérons la classe A avec la dépendance B, qui à son tour a la dépendance C, qui à son tour a la dépendance D. Sans DI, A construit B, B construit C, C construit D. Avec construction construction, pour construire A, vous devez d'abord construire B, pour construire B, vous devez d'abord construire C, et pour construire C, vous devez d'abord construire D. Ainsi, la classe A doit maintenant connaître D, la dépendance d'une dépendance d'une dépendance, afin de construire B. Cela conduit à un couplage excessif.
Theodore Murdock
1
Avoir un constructeur supplémentaire utilisé uniquement pour les tests et permettant d'injecter les dépendances ne coûterait pas tellement cher. Je vais essayer de réviser ce que j'ai dit.
Theodore Murdock
3
Le dosage des dépendances d’injection déplace uniquement les dépendances dans un jeu à somme nulle. Vous déplacez vos dépendances dans des endroits où elles existent déjà: le projet principal. Vous pouvez même utiliser des approches conventionnelles pour vous assurer que les dépendances ne sont résolues qu'au moment de l'exécution, par exemple en spécifiant quelles classes sont concrètes par des espaces de noms ou autre. Je dois dire que c'est une réponse naïve
Sprague
2
@Sprague Dependency injection déplace les dépendances dans un jeu à somme légèrement négative, si vous l'appliquez dans les cas où il ne devrait pas être appliqué. Le but de cette réponse est de rappeler aux gens que l’injection de dépendance n’est pas intrinsèquement bonne, c’est un modèle de conception incroyablement utile dans les bonnes circonstances, mais un coût injustifiable dans des circonstances normales (du moins dans les domaines de programmation dans lesquels j'ai travaillé). Je comprends que dans certains domaines, il est plus souvent utile que dans d’autres). DI devient parfois un mot à la mode et une fin en soi, ce qui passe à côté de la question.
Theodore Murdock
11
  1. 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,

  2. 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.

  3. 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

0lukasz0
la source
Tous les points concernent l'utilisation d'une forme d'injection différente plutôt que de ne pas l'utiliser complètement. +1 pour le lien si.
Jan Hudec
3
Service Locator n'est PAS un anti-modèle et le type de blog auquel vous êtes associé n'est pas un expert en modèles. Le fait qu’il ait obtenu la réputation de StackExchange est en quelque sorte un mauvais service rendu à la conception de logiciels. Martin Fowler (auteur de Patterns of Enterprise Application Architecture) a une vision plus raisonnable de la question: martinfowler.com/articles/injection.html
Colin
1
@Colin, au contraire, Service Locator est très certainement un anti-motif, et voici en un mot la raison .
Kyralessa le
@ Kyralessa - vous vous rendez compte que les frameworks DI utilisent en interne un modèle de localisateur de services pour rechercher des services, n'est-ce pas? Il existe des cas où les deux sont appropriés, et aucun modèle anti-modèle en dépit de la haine de StackExchange que certaines personnes souhaitent vider.
Colin
Bien sûr, ma voiture utilise en interne des pistons, des bougies d’allumage et bien d’autres choses qui ne constituent toutefois pas une bonne interface pour un être humain ordinaire qui veut seulement conduire une voiture.
Kyralessa le
8

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.

Ed James
la source
Je ne comprends pas ce que vous entendez par journalisation. Il est évident que tous les enregistreurs ne se conformeront pas à la même interface, vous devrez donc écrire votre propre wrapper pour traduire entre la manière de journalisation de ce projet et un enregistreur spécifique (en supposant que vous souhaitiez pouvoir changer facilement d'enregistreurs). Alors après, qu'est-ce que DI vous donne?
Richard Tingle
@RichardTingle Je dirais que vous devez définir votre wrapper de journalisation en tant qu'interface, puis écrire une implémentation légère de cette interface pour chaque service de journalisation externe, au lieu d'utiliser une seule classe de journalisation qui encapsule plusieurs abstractions. C'est le même concept que vous proposez mais qui est résumé à un niveau différent.
Ed James
1

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.

  1. 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.

  2. 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.

  3. 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.)

tymtam
la source
1

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.

Koray Tugay
la source
Je ne vois pas en quoi le temps réel de la demande liée à DI
Michael Freidgeim
@MichaelFreidgeim Il faut du temps pour initialiser le contexte, généralement les conteneurs DI sont assez lourds, comme Spring. Créez une application Hello World avec une seule classe, créez-en une avec Spring et commencez les deux fois 10 fois. Vous verrez ce que je veux dire.
Koray Tugay
Cela ressemble à un problème avec un conteneur DI individuel. Dans le monde Net, je n'ai pas entendu parler du temps d'initialisation en tant que problème essentiel pour les conteneurs DI
Michael Freidgeim
1

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:

  1. Vous avez plus d'interfaces (plus de types)
  2. Vous avez créé des doublons de signatures de méthodes publiques et vous devrez le doubler (vous ne pouvez pas changer un paramètre une fois, vous devrez le faire deux fois, essentiellement toutes les possibilités de refactoring et de navigation deviennent plus compliquées).
  3. Vous avez déplacé certaines erreurs de compilation pour exécuter des échecs de temps (avec DI, vous pouvez simplement ignorer certaines dépendances lors du codage et ne pas être sûr qu'elles seront exposées lors des tests).
  4. Vous avez ouvert votre encapsulation. Maintenant, les membres protégés, les types internes, etc. sont devenus publics
  5. La quantité globale de code a été augmentée

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

Eugene Gorbovoy
la source
0

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.

Garrett Hall
la source
12
Le localisateur de service masque les dépendances dans votre code . Ce n'est pas un bon modèle à utiliser.
Kyralessa
En ce qui concerne les dépendances statiques, je préférerais voir des façades leur implémenter des interfaces. Celles-ci peuvent alors être une dépendance injectée, et elles tombent pour vous sur la grenade statique.
Erik Dietrich
@Kyralessa Je suis d'accord, un localisateur de services présente de nombreux inconvénients et la DI est presque toujours préférable. Cependant, je pense qu'il existe quelques exceptions à la règle, comme pour tous les principes de programmation.
Garrett Hall
La principale utilisation acceptée de l'emplacement de service se trouve dans un modèle de stratégie, dans lequel une classe de "sélecteur de stratégie" dispose d'une logique suffisante pour trouver la stratégie à utiliser et la restituer ou lui passer un appel. Même dans ce cas, le sélecteur de stratégie peut être une façade pour une méthode d'usine fournie par un conteneur IoC, qui reçoit la même logique; la raison pour laquelle vous voulez l'expliquer est de mettre la logique à l'endroit où elle appartient et non à l'endroit où elle est le plus cachée.
KeithS
Pour citer Martin Fowler, "le choix entre (DI et Service Locator) est moins important que le principe de séparation de la configuration de l'utilisation". L'idée que DI est TOUJOURS mieux, c'est de la foutaise. Si un service est utilisé dans le monde entier, l'utilisation de DI est plus lourde et fragile. De plus, DI est moins favorable pour les architectures de plug-in où les implémentations ne sont pas connues avant l'exécution. Imaginez combien il serait difficile de toujours passer des arguments supplémentaires à Debug.WriteLine (), etc.
Colin