web-dev-qa-db-fra.com

Critique et inconvénients de l'injection de dépendance

L'injection de dépendance (DI) est un modèle bien connu et à la mode. La plupart des ingénieurs connaissent ses avantages, comme:

  • Rendre l'isolement possible/facile dans les tests unitaires
  • Définition explicite des dépendances d'une classe
  • Faciliter une bonne conception ( principe de responsabilité unique (SRP) par exemple)
  • Activation rapide des implémentations de commutation (DbLogger au lieu de ConsoleLogger par exemple)

Je pense qu'il existe un large consensus dans l'industrie selon lequel l'ID est 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 classes
  • Création d'interfaces inutiles

Actuellement, nous discutons de la conception de l'architecture avec mon collègue. Il est assez conservateur, mais ouvert d'esprit. Il aime remettre en question des choses que je considère bonnes, car de nombreuses personnes en informatique copient simplement la tendance la plus récente, répètent les avantages et en général ne pensent pas trop - n'analysent pas trop profondément.

Je voudrais demander:

  • Devrions-nous utiliser l'injection de dépendances lorsque nous n'avons qu'une seule implémentation?
  • Faut-il interdire la création de nouveaux objets sauf ceux de langage/framework?
  • Injecter une seule implémentation est une mauvaise idée (disons que nous n'avons qu'une seule implémentation, donc nous ne voulons pas créer d'interface "vide") si nous ne prévoyons pas de tester unitairement une classe particulière?
130
Landeeyo

Tout d'abord, je voudrais séparer l'approche de conception du concept de cadres. L'injection de dépendance à son niveau le plus simple et le plus fondamental est simplement:

Un objet parent fournit toutes les dépendances requises à l'objet enfant.

C'est ça. Notez que rien dans cela ne nécessite d'interfaces, de frameworks, de style d'injection, etc. Pour être honnête, j'ai découvert ce modèle il y a 20 ans. Ce n'est pas nouveau.

En raison de plus de 2 personnes ayant une confusion sur le terme parent et enfant, dans le contexte de l'injection de dépendance:

  • Le parent est l'objet qui instancie et configure l'objet enfant qu'il utilise
  • L'enfant est le composant conçu pour être instancié passivement. C'est à dire. il est conçu pour utiliser toutes les dépendances fournies par le parent et n'instancie pas ses propres dépendances.

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 à quel point deux objets peuvent être étroitement couplés. Toutes les dépendances n'ont pas besoin d'une interface, mais elles aident à écrire du 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 simulé ou tronqué que vous utilisez pour tester le code qui dépend de l'objet. Cela seul peut suffire à justifier l'interface.

Pourquoi les frameworks?

Initialiser et fournir des dépendances à des objets enfants peut être intimidant lorsqu'il y en a un grand nombre. Les cadres offrent les avantages suivants:

  • Dépendances automatiques des composants
  • Configuration des composants avec des paramètres d'une sorte
  • Automatiser le code de la plaque de la chaudière pour que vous n'ayez pas à le voir écrit à plusieurs endroits.

Ils présentent également les inconvénients suivants:

  • L'objet parent est un "conteneur", et rien dans votre code
  • Cela rend les tests plus compliqués si vous ne pouvez pas fournir les dépendances directement dans votre code de test
  • Il peut ralentir l'initialisation car il résout toutes les dépendances à l'aide de la réflexion et de nombreuses autres astuces
  • Le débogage d'exécution peut être plus difficile, en particulier si le conteneur injecte un proxy entre l'interface et le composant réel qui implémente l'interface (la programmation orientée aspect intégrée à Spring vient à l'esprit). Le conteneur est une boîte noire, et ils ne sont pas toujours construits avec un concept facilitant le processus de débogage.

Cela dit, il y a des compromis. Pour les petits projets où il n'y a pas beaucoup de pièces mobiles et où il y a peu de raisons d'utiliser un framework DI. Cependant, pour des projets plus complexes où certains composants sont déjà faits pour vous, le cadre peut être justifié.

Qu'en est-il de [article aléatoire sur Internet]?

Qu'en est-il? Plusieurs fois, les gens peuvent devenir trop zélés et ajouter un tas de restrictions et vous réprimander si vous ne faites pas les choses de la "vraie façon". Il n'y a pas de vrai moyen. Voyez si vous pouvez extraire quelque chose d'utile de l'article et ignorer les choses avec lesquelles vous n'êtes pas d'accord.

Bref, pensez par vous-même et essayez les choses.

Travailler avec des "vieilles têtes"

Apprenez autant que vous le pouvez. Ce que vous trouverez avec beaucoup de développeurs qui travaillent dans leurs années 70, c'est qu'ils ont appris à ne pas être dogmatiques à propos de beaucoup de choses. Ils ont des méthodes avec lesquelles ils ont travaillé pendant 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.

168
Berin Loritsch

L'injection de dépendance est, comme la plupart des modèles, une solution aux problèmes. Commencez donc par demander si vous avez même le problème en premier lieu. Si ce n'est pas le cas, l'utilisation du modèle le plus probable aggravera le code .

Considérez d'abord si vous pouvez réduire ou éliminer les dépendances. Toutes choses étant égales par ailleurs, nous voulons 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 théorique!

Considérons un module qui télécharge des données à partir d'un service externe, les analyse et effectue des analyses complexes et écrit pour aboutir dans un fichier.

Maintenant, si la dépendance au service externe est codée en dur, alors il sera vraiment difficile de tester à l'unité 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 à la place, ce qui rend possible le test unitaire de la logique interne.

Mais une bien meilleure solution consiste simplement à séparer l'analyse de l'entrée/sortie. Si l'analyse est extraite vers un module sans effets secondaires, il sera beaucoup plus facile de tester. Notez que la simulation est une odeur de code - elle n'est pas toujours évitable, mais en général, il est préférable de pouvoir tester sans se fier à la simulation. Ainsi, en éliminant les dépendances, vous évitez les problèmes que DI est censé atténuer. Notez qu'une telle conception adhère également beaucoup mieux à SRP.

Je tiens à souligner que la DI ne facilite pas nécessairement la SRP ou d'autres bons principes de conception comme la séparation des préoccupations, la forte cohésion/faible couplage, etc. Cela pourrait tout 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 d'implémentation. Si vous changez cela pour injecter B dans le constructeur de A, alors vous avez exposé ce détail d'implémentation et maintenant les connaissances sur cette dépendance et sur la façon d'initialiser B, la durée de vie de B et ainsi de suite doivent exister séparément à un autre endroit du système. de A. Vous avez donc une architecture globale pire avec des problèmes de fuite.

D'un autre côté, il y a des cas où DI est vraiment utile. Par exemple pour les services globaux avec des effets secondaires comme un enregistreur.

Le problème est lorsque les modèles et les architectures deviennent des objectifs en soi plutôt que des outils. Juste demander "Devrions-nous utiliser DI?" c'est en quelque sorte 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 réalisez probablement déjà la réponse à cette question - absolument pas! Quiconque vous dit le contraire essaie de vous vendre quelque chose - probablement des heures de consultation coûteuses. Une interface n'a de valeur que si elle représente une abstraction. Une interface qui imite simplement la surface d'une seule classe est appelée "interface d'en-tête" et c'est un antipattern connu.

92
JacquesB

D'après mon expérience, il y a un certain nombre d'inconvénients à l'injection de dépendance.

Premièrement, l'utilisation de DI ne simplifie pas autant les tests automatisés que ce qui est annoncé. Le test unitaire d'une classe avec une implémentation simulée d'une interface vous permet de valider comment cette classe va interagir avec l'interface. Autrement dit, il vous permet de tester de façon unitaire comment la classe testée utilise le contrat fourni par l'interface. Cependant, cela donne une bien meilleure assurance que l'entrée de la classe testée dans l'interface est comme prévu. Il fournit une assurance plutôt médiocre que la classe testée répond comme prévu à la sortie de l'interface, car il s'agit d'une sortie presque universellement simulée, elle-même sujette à des bogues, à des simplifications excessives, etc. En bref, cela ne vous permet PAS de valider que la classe se comportera comme prévu avec une véritable implémentation de l'interface.

Deuxièmement, DI rend la navigation dans le code beaucoup plus difficile. Lorsque vous essayez de naviguer vers la définition de classes utilisées comme entrée de fonctions, une interface peut être quelque chose d'une gêne mineure (par exemple, lorsqu'il n'y a qu'une seule implémentation) à un puits de temps majeur (par exemple, lorsqu'une interface trop générique comme IDisposable est utilisée) lorsque vous essayez de trouver l'implémentation réelle utilisée. Cela peut transformer un exercice simple comme "J'ai besoin de corriger une exception de référence nulle dans le code qui se produit juste après l'impression de cette instruction de journalisation" en un effort d'une journée.

Troisièmement, l'utilisation de DI et de cadres est une arme à double tranchant. Il peut réduire considérablement la quantité de code de plaque de chaudière nécessaire pour les opérations courantes. Cependant, cela se fait au détriment de la nécessité d'avoir une connaissance détaillée du cadre DI particulier pour comprendre comment ces opérations courantes sont réellement câblées ensemble. Comprendre comment les dépendances sont chargées dans le framework et ajouter une nouvelle dépendance dans le framework à injecter peut nécessiter la lecture d'une bonne quantité de documentation et suivre quelques tutoriels de base sur le framework. Cela peut transformer certaines tâches simples en tâches assez longues.

37
Eric

J'ai suivi les conseils de Mark Seemann de "l'injection de dépendance dans .NET" - pour conjecturer.

DI doit être utilisé lorsque vous avez une `` dépendance volatile '', par ex. il y a une chance raisonnable que cela change.

Donc, si vous pensez que vous pourriez avoir plus d'une implémentation à l'avenir ou que l'implémentation pourrait changer, utilisez DI. Sinon, new est très bien.

15
Rob

Ma plus grande bête noire à propos de DI a déjà été mentionnée dans quelques réponses, mais je vais en parler un peu ici. DI (comme cela se fait principalement aujourd'hui, avec des conteneurs, etc.) nuit vraiment à la lisibilité du code. Et la lisibilité du code est sans doute la raison derrière la plupart des innovations de programmation d'aujourd'hui. Comme quelqu'un l'a dit, il est facile d'écrire du code. La lecture du code est difficile. Mais c'est aussi extrêmement important, à moins que vous n'écriviez une sorte de petit utilitaire jetable à écriture unique.

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 partage-je 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 tracer le flux du programme sans l'exécuter et simplement à travers pour voir exactement quelle implémentation de l'interface a été utilisée à cet endroit particulier. Et parfois, il y a un obstacle technique qui vous empêche de passer à travers. Et même si vous le pouvez, si cela implique de passer par les entrailles tordues du conteneur DI, l'ensemble l'affaire devient rapidement un exercice de frustration.

Pour travailler efficacement avec un morceau de code qui utilisait DI, vous devez être familier avec lui et déjà savoir ce qui va où.

7
Vilx-

Activation rapide des implémentations de commutation (DbLogger au lieu de ConsoleLogger par exemple)

Bien que DI en général soit sûrement une bonne chose, je suggère de ne pas l'utiliser aveuglément pour tout. Par exemple, je n'injecte jamais d'enregistreurs. L'un des avantages de DI est de rendre les dépendances explicites et claires. Il n'y a aucun intérêt à répertorier ILogger comme dépendance de presque toutes les classes - c'est juste de l'encombrement. Il est de la responsabilité de l'enregistreur de fournir la flexibilité dont vous avez besoin. Tous mes enregistreurs sont des membres finaux statiques, je peux envisager d'injecter un enregistreur lorsque j'en ai besoin d'un non statique.

Augmentation du nombre de classes

C'est un inconvénient du cadre DI donné ou du cadre moqueur, pas du DI lui-même. Dans la plupart des endroits, mes classes dépendent de classes concrètes, ce qui signifie qu'il n'y a pas besoin de passe-partout. Guice (un Java DI) lie par défaut une classe à elle-même et j'ai seulement besoin de remplacer la liaison dans les tests (ou de les câbler manuellement à la place)).

Création d'interfaces inutiles

Je ne crée les interfaces qu'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 le IDE peut le faire pour moi.

Devrions-nous utiliser l'injection de dépendances lorsque nous n'avons qu'une seule implémentation?

Oui, mais éviter toute plaque supplémentaire .

Faut-il interdire la création de nouveaux objets sauf ceux de langage/framework?

Non. Il y aura de nombreuses classes de valeur (immuables) et de données (mutables), où les instances seront simplement créées et transmises et où il n'y a aucun intérêt à les injecter - car elles ne sont jamais stockées dans un autre objet (ou seulement dans d'autres ces objets).

Pour eux, vous devrez peut-être injecter une usine à la place, 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'y a rien à injecter).

Injecter une seule implémentation est une mauvaise idée (disons que nous n'avons qu'une seule implémentation, donc nous ne voulons pas créer d'interface "vide") si nous ne prévoyons pas de tester unitairement une classe particulière?

À mon humble avis, c'est bien tant qu'il ne provoque pas de ballonnement de code. Injectez la dépendance, mais ne créez pas l'interface (et pas de configuration XML folle!) Comme vous pouvez le faire plus tard sans tracas.

En fait, dans mon projet actuel, il y a quatre classes (sur des centaines), que j'ai décidé de exclure de DI car ce sont de simples classes utilisées dans trop d'endroits, y compris les objets de données.


Un autre inconvénient de la plupart des infrastructures DI est la surcharge d'exécution. Cela peut être déplacé pour compiler le temps (pour Java, il y a Dagger , aucune idée sur les autres langages).

Un autre inconvénient est la magie qui se produit partout, qui peut être réduite (par exemple, j'ai désactivé la création de proxy lors de l'utilisation de Guice).

3
maaartinus