web-dev-qa-db-fra.com

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

Depuis l'apprentissage (et l'amour) des 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?

133
Tom Squires

Fondamentalement, l'injection de dépendance émet certaines hypothèses (généralement mais pas toujours valides) sur la nature de vos objets. Si ceux-ci sont erronés, l'ID n'est peut-être pas la meilleure solution:

  • Tout d'abord, le plus fondamentalement, DI suppose que le couplage étroit des implémentations d'objets est TOUJOURS mauvais. C'est l'essence du principe d'inversion de la dépendance: "une dépendance ne doit jamais être faite sur une concrétion; seulement sur une abstraction".

    Cela ferme l'objet dépendant à modifier en fonction d'une modification de l'implémentation concrète; une classe dépendant spécifiquement de ConsoleWriter devra changer si la sortie doit aller dans un fichier à la place, mais si la classe dépend uniquement d'un IWriter exposant une méthode Write (), nous pouvons remplacer le ConsoleWriter actuellement utilisé par un FileWriter et notre 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 (), un objet de code supplémentaire (l'interface IWriter) doit maintenant être modifié, en plus de l'objet/méthode d'implémentation et de ses utilisations. Si des changements dans l'interface réelle sont plus probables que des changements dans la mise en œuvre de ladite interface, un couplage lâche (et des dépendances à couplage lâche DI) peuvent causer 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 du code qui crée une dépendance et l'utilise également, il y a deux raisons pour lesquelles la classe dépendante peut avoir à changer (une modification de l'utilisation OR l'implémentation), 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 la logique est la seule implémentation d'une telle dépendance, alors il est plus difficile de coder la résolution faiblement couplée de la dépendance (injection, emplacement de service, usine) qu'elle ne le serait pour utiliser simplement new et l'oublier.

  • Enfin, DI par sa nature centralise la connaissance de toutes les dépendances ET de leurs implémentations. Cela augmente le nombre de références que l'assembly qui effectue l'injection doit avoir et, dans la plupart des cas, ne réduit PAS le nombre de références requises 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 l'implémentation de la dépendance afin de "connecter les points" et de 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 le formulaire principal 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 couplé et beaucoup de références d'assembly à des niveaux élevés de votre application, qui n'a besoin que de ces connaissances pour le "cacher" des classes dépendantes réelles (qui, d'un point de vue très basique, est le meilleur endroit pour avoir ces connaissances; où les utiliser).

    Il ne supprime pas non plus 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 à l'un des trois emplacements:

    • le tout dans un seul assemblage "Interfaces" qui devient très centré sur l'application,
    • chacun aux côtés de la ou des implémentations principales, supprimant ainsi l'avantage de ne pas avoir à recompiler les dépendances lorsque les dépendances changent, ou
    • un ou deux 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'y en a peut-être pas.

124
KeithS

En dehors des infrastructures d'injection de dépendance, l'injection de dépendance (via l'injection de constructeur ou l'injection de setter) est presque un jeu à somme nulle: vous diminuez le couplage entre l'objet A et sa dépendance B, mais maintenant tout objet qui a besoin d'une instance de A doit maintenant construire é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.

L'injection de dépendances (sans framework) est donc tout aussi nuisible qu'utile.

Cependant, le coût supplémentaire est souvent facilement justifiable: si le code client sait plus comment construire la dépendance que l'objet lui-même, alors l'injection de dépendance réduit vraiment le couplage; par exemple, un scanner ne sait pas très bien comment obtenir ou construire un flux d'entrée à partir duquel analyser, ou de quelle source le code client veut analyser l'entrée, donc l'injection par le constructeur d'un flux d'entrée est la solution évidente.

Le test est une autre justification, afin de pouvoir utiliser les fausses dépendances. Cela devrait signifier l'ajout d'un constructeur supplémentaire utilisé pour les tests uniquement qui permet d'injecter des dépendances: si vous changez vos constructeurs pour toujours exiger que les dépendances soient injectées, tout à coup, vous devez connaître les dépendances de vos dépendances afin de construire votre dépendances directes, et vous ne pouvez faire aucun travail.

Cela peut être utile, mais vous devriez certainement vous demander pour chaque dépendance, l'avantage du test en vaut-il le coût, et vais-je vraiment vouloir me moquer de cette dépendance pendant le test?

Lorsqu'une infrastructure d'injection de dépendances est ajoutée et que la construction des dépendances est déléguée non pas au code client mais à la structure, 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 comptez et de transférer la responsabilité de décider de la dépendance sur laquelle vous comptez à 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 prescrit quel fournisseur doit être utilisé pour chaque ressource, ou à un hybride des deux (par exemple, il peut y avoir être un processus de résolution automatisé des dépendances qui peut être remplacé, si nécessaire, à l'aide d'un fichier de configuration).

Comme pour l'injection de constructeur, je pense que l'avantage de le faire finit, encore une fois, par être très similaire au coût de le faire: vous n'avez pas besoin de savoir qui fournit les données sur lesquelles vous comptez et, s'il y a plusieurs potentiels fournisseurs, vous n'avez pas besoin de connaître l'ordre préféré pour vérifier les fournisseurs, assurez-vous que chaque emplacement qui a besoin des données vérifie 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 qu'ils offrent plus d'avantages que de coût lorsque le mal de tête de trouver le bon fournisseur des données ou du service dont vous avez besoin a un coût plus élevé que le mal de tête, quand quelque chose échoue, de ne pas savoir immédiatement localement quel code a fourni les mauvaises données qui ont provoqué un échec ultérieur dans votre code.

Dans certains cas, d'autres modèles qui masquent les dépendances (par exemple, les localisateurs de services) avaient déjà été adoptés (et ont peut-être également prouvé leur valeur) lorsque les cadres DI sont apparus sur la scène, et les cadres DI ont été adoptés parce qu'ils offraient un avantage concurrentiel, comme moins de code passe-partout, ou potentiellement faire moins pour obscurcir le fournisseur de dépendance lorsqu'il devient nécessaire de déterminer quel fournisseur est réellement utilisé.

30
Theodore Murdock
  1. si vous créez des entités de base de données, vous devriez plutôt avoir une classe d'usine que vous injecterez à la place dans votre contrôleur,

  2. si vous devez créer des objets primitifs comme des pouces ou des longs. Vous devez également créer "à la main" la plupart des objets de bibliothèque standard comme les dates, les guides, etc.

  3. si vous souhaitez injecter des chaînes de configuration, il est probablement préférable d'injecter certains 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 le localisateur de service comme alternative d'injection de dépendance, c'est anti-modèle, plus d'informations: http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx

11
0lukasz0

Lorsque vous ne gagnez rien en rendant votre projet maintenable et testable.

Sérieusement, j'adore l'IoC et la 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-utilisateurs, où votre code peut être réutilisé encore et encore 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 maintenable 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 modèle applicable. Par exemple, les points d'entrée fonctionnels qui sont 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 peut tout simplement pas avoir de dépendances injectées lors de l'initialisation temps. J'irais aussi jusqu'à dire qu'un prototype, ou n'importe quel morceau de code d'enquête jetable, est également un mauvais candidat; les avantages de l'ID sont à peu près des avantages à moyen et à long terme (testabilité et maintenabilité), si vous êtes certain que vous jeterez la majorité d'un morceau de code dans une semaine environ, je dirais que vous ne gagnez rien en isolant dépendances, passez simplement le temps que vous passez normalement à tester et à isoler les dépendances pour faire fonctionner le code.

Dans l'ensemble, il est judicieux d'adopter une approche pragmatique à toute méthodologie ou modèle, car rien n'est applicable à 100% du temps.

Une chose à noter est votre commentaire sur les tests automatisés: ma définition est des tests fonctionnels automatisés, par exemple des tests Selenium scriptés si vous êtes dans un contexte Web. Ce sont généralement des tests entièrement en boîte noire, sans avoir besoin de connaître le fonctionnement interne du code. Si vous parliez de 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, par exemple, il vous permet de tester des choses comme méthodes qui touchent la base de données sans qu'il soit nécessaire qu'une base de données soit présente.

8
Ed James

Ce n'est pas une réponse complète, mais juste un autre point.

Lorsque vous avez une application qui démarre une fois, s'exécute pendant longtemps (comme une application Web) DI peut être bon.

Lorsque vous avez une application qui démarre plusieurs fois et s'exécute pendant des périodes plus courtes (comme une application mobile), vous ne voulez probablement pas le conteneur.

2
Koray Tugay

Essayez d'utiliser les principes de base OOP: utilisez l'héritage pour extraire les fonctionnalités courantes, encapsuler (cacher) les choses qui devraient être protégées du monde extérieur en utilisant des 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 .

Ensuite essayez de le réécrire avec DI, et comparez le code, ce que vous verrez généralement 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 simplement changer un paramètre une fois, vous devrez le faire deux fois, essentiellement toutes les possibilités de refactoring et de navigation devenant plus compliquées)
  3. Vous avez déplacé quelques erreurs de compilation pour exécuter des échecs de temps (avec DI, vous pouvez simplement ignorer certaines dépendances pendant le codage et pas sûr qu'elles seront exposées pendant les 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

Je dirais que d'après ce que j'ai vu, presque toujours, la qualité du code diminue avec DI.

Cependant, si vous utilisez uniquement un modificateur d'accès "public" dans la déclaration de classe et/ou des modificateurs publics/privés pour les membres, et/ou si vous n'avez pas le choix d'acheter des outils de test coûteux et en même temps, vous avez besoin de tests unitaires qui peuvent ' t être remplacé 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!

p.s. j'obtiendrai probablement beaucoup d'inconvénients pour ce post, je crois parce que la plupart des développeurs modernes ne comprennent tout simplement pas comment et pourquoi utiliser mot-clé interne et comment diminuer le couplage de vos composants et enfin pourquoi le diminuer) enfin, essayez de coder et de comparer

1
Eugene Gorbovoy

Alors que les autres réponses se concentrent sur les aspects techniques, je voudrais ajouter une dimension pratique.

Au fil des ans, je suis parvenu à la conclusion que plusieurs conditions pratiques doivent être remplies pour que l'introduction de l'injection de dépendance soit un succès.

  1. Il devrait y avoir une raison de l'introduire.

    Cela semble évident, mais si votre code obtient uniquement des éléments de la base de données et les renvoie sans aucune logique, l'ajout d'un conteneur DI rend les choses plus complexes sans réel avantage. Les tests d'intégration seraient plus importants ici.

  2. L'équipe doit être formée et embarquée.

    À moins que la majorité de l'équipe ne soit à bord et ne comprenne que l'ajout de l'inversion du conteneur de contrôle DI devient une autre façon de faire les choses et rend la base de code encore plus compliquée.

    Si l'ID est introduite par un nouveau membre de l'équipe, parce qu'ils la comprennent et l'aiment et veulent simplement montrer qu'ils sont bons, ET que l'équipe n'est pas activement impliquée, il y a un risque réel que cela diminue réellement la qualité de 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 moment de la compilation au moment de l'exécution. C'est en fait assez dangereux si vous ne testez pas bien. Les échecs de résolution du 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 testent pas dans la mesure requise par DI.)

1
tymtam

Un alternative à l'injection de dépendance utilise un localisateur de service . Un localisateur de service est plus facile à comprendre, à déboguer et rend la construction d'un objet plus simple, surtout si vous n'utilisez pas d'infrastructure DI. Les localisateurs de service sont un bon modèle pour gérer les dépendances externes statique, par exemple une base de données que vous auriez autrement dû passer dans chaque objet de votre couche d'accès aux données.

Lorsque refactorise le code hérité , il est souvent plus facile de refactoriser vers un localisateur de service que vers 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, il existe certains inconvénients du localisateur de services. Connaître les dépendances d'une classe est plus difficile car les dépendances sont cachées dans l'implémentation de la classe, pas dans les constructeurs ou les setters. Et la création de deux objets qui reposent sur des implémentations différentes du même service est difficile, voire impossible.

[~ # ~] tldr [~ # ~] : si votre classe a des dépendances statiques ou si vous refactorisez le code hérité, un localisateur de service est sans doute meilleur que DI.

0
Garrett Hall