Combien coûte trop d'injection de dépendance?

38

Je travaille dans un projet qui utilise (Spring) Dependency Injection pour littéralement tout ce qui est une dépendance d'une classe. Nous sommes à un point où le fichier de configuration de Spring a atteint environ 4000 lignes. Il y a peu de temps, j'ai regardé l'un des discours de l'oncle Bob sur YouTube (malheureusement, je n'ai pas trouvé le lien) dans lequel il recommande de n'injecter que quelques dépendances centrales (par exemple, des usines, une base de données,…) dans le composant principal à partir duquel être distribué.

Les avantages de cette approche sont le découplage du cadre DI de la majeure partie de l'application et rend également la configuration de Spring plus propre, car les usines contiendront beaucoup plus de ce qui était auparavant. Au contraire, cela entraînera une propagation de la logique de création parmi de nombreuses classes d’usines et les tests pourraient devenir plus difficiles.

Ma question est donc la suivante: quels autres avantages ou inconvénients voyez-vous dans l’une ou l’autre approche? Existe-t-il des meilleures pratiques? Merci beaucoup pour vos réponses!

Antimon
la source
3
Avoir un fichier de configuration de 4000 lignes n'est pas la faute d'une injection de dépendance ... il existe de nombreuses façons de le réparer: modulez le fichier en plusieurs fichiers plus petits, passez à l'injection basée sur des annotations, utilisez des fichiers JavaConfig au lieu de xml. Fondamentalement, le problème rencontré est l’incapacité de gérer la complexité et la taille des besoins d’injection de dépendance de votre application.
Maybe_Factor
1
@ Maybe_Factor TL; DR divise le projet en composants plus petits.
Walfrat

Réponses:

44

Comme toujours, ça dépend ™. La réponse dépend du problème que l'on tente de résoudre. Dans cette réponse, je vais essayer de parler de certaines forces de motivation communes:

Privilégiez les bases de code plus petites

Si vous avez 4 000 lignes de code de configuration Spring, je suppose que la base de code contient des milliers de classes.

Ce n'est pas un problème que vous pouvez régler après coup, mais en règle générale, j'ai tendance à préférer les applications plus petites, avec des bases de code plus petites. Si vous êtes intéressé par la conception par domaine , vous pouvez, par exemple, créer une base de code par contexte lié.

Je fonde ce conseil sur mon expérience limitée, car j’ai écrit le code de secteur d’activité sur le Web pendant la majeure partie de ma carrière. J'imagine que, si vous développez une application de bureau, un système intégré ou autre, les choses sont plus difficiles à séparer.

Bien que je réalise que ce premier conseil est de loin le moins pratique, je pense aussi que c’est le plus important, et c’est pourquoi je l’inclus. La complexité du code varie de manière non linéaire (éventuellement exponentielle) avec la taille de la base de code.

Favor Pure DI

Bien que je sache toujours que cette question présente une situation existante, je recommande Pure DI . N'utilisez pas de conteneur DI, mais si vous le faites, utilisez-le au moins pour implémenter une composition basée sur des conventions .

Je n'ai aucune expérience pratique de Spring, mais je suppose que par fichier de configuration , un fichier XML est impliqué.

La configuration des dépendances à l'aide de XML est le pire des deux mondes. Tout d'abord, vous perdez la sécurité du type compilation, mais vous ne gagnez rien. Un fichier de configuration XML peut facilement être aussi gros que le code qu’il tente de remplacer.

Comparés au problème qu’il est censé résoudre, les fichiers de configuration de l’injection de dépendance occupent une mauvaise place dans l’ horloge de complexité de la configuration .

Le cas de l'injection de dépendance à grain grossier

Je peux plaider en faveur d'une injection de dépendance à grain grossier. Je peux également plaider en faveur d'une injection de dépendance à grain fin (voir la section suivante).

Si vous n'injectez que quelques dépendances «centrales», la plupart des classes peuvent ressembler à ceci:

public class Foo
{
    private readonly Bar bar;

    public Foo()
    {
        this.bar = new Bar();
    }

    // Members go here...
}

Ceci correspond toujours à la composition d ' objets privilégiée de Design Patterns par rapport à l' héritage de classe , car Foocompose Bar. Du point de vue de la maintenabilité, cela pourrait toujours être considéré comme maintenable, car si vous devez modifier la composition, vous devez simplement modifier le code source Foo.

C'est à peine moins maintenable que l'injection de dépendance. En fait, je dirais qu'il est plus facile de modifier directement la classe utilisée Bar, au lieu de devoir suivre l'indirection inhérente à l'injection de dépendance.

Dans la première édition de mon livre sur l'injection de dépendance , je fais la distinction entre dépendances volatiles et stables.

Les dépendances volatiles sont ces dépendances que vous devriez envisager d'injecter. Ils incluent

  • Dépendances qui doivent être reconfigurables après la compilation
  • Dépendances développées en parallèle par une autre équipe
  • Dépendances à comportement non déterministe ou à effets secondaires

Les dépendances stables, en revanche, sont des dépendances qui se comportent de manière bien définie. En un sens, vous pourriez faire valoir que cette distinction justifie l'injection de dépendance à grain grossier, bien que je dois avouer que je ne l'avais pas entièrement compris lorsque j'ai écrit le livre.

Du point de vue des tests, toutefois, cela rend les tests unitaires plus difficiles. Vous ne pouvez plus effectuer de test unitaire Fooindépendamment de Bar. Comme l' explique JB Rainsberger , les tests d'intégration souffrent d'une explosion combinatoire de complexité. Vous devrez littéralement écrire des dizaines de milliers de scénarios de test si vous souhaitez couvrir tous les chemins grâce à une intégration de 4 à 5 classes.

Le contre-argument à cela est que souvent, votre tâche n'est pas de programmer une classe. Votre tâche consiste à développer un système qui résout certains problèmes spécifiques. C'est la motivation derrière le développement dirigé par le comportement (BDD).

DHH présente un autre point de vue à ce sujet, affirmant que le TDD entraîne des dommages de conception induits par les tests . Il est également favorable aux tests d'intégration à granularité grossière.

Si vous prenez cette perspective sur le développement logiciel, alors l’injection de dépendance grossière a du sens.

Le cas de l'injection de dépendance à grain fin

L’injection de dépendance fine, par contre, pourrait être décrite comme une injection de toutes les choses!

Ma principale préoccupation concernant l'injection de dépendance à grain grossier concerne les critiques formulées par JB Rainsberger. Vous ne pouvez pas couvrir tous les chemins de code avec des tests d'intégration, car vous devez écrire littéralement des milliers, voire des dizaines de milliers de scénarios de test pour couvrir tous les chemins de code.

Les partisans de BDD répondront avec l'argument qu'il n'est pas nécessaire de couvrir tous les chemins de code avec des tests. Vous devez seulement couvrir ceux qui produisent de la valeur commerciale.

D'après mon expérience, cependant, tous les chemins de code «exotiques» seront également exécutés dans un déploiement à volume élevé et, s'ils ne sont pas testés, nombre d'entre eux présenteront des défauts et entraîneront des exceptions d'exécution (souvent des exceptions null-reference).

Cela m'a amené à préférer l'injection de dépendances à granularité fine, car cela me permet de tester les invariants de tous les objets de manière isolée.

Favoriser la programmation fonctionnelle

Bien que je me penche pour une injection de dépendance fine, je me suis concentré sur la programmation fonctionnelle, entre autres parce qu'elle est intrinsèquement testable .

Plus vous avancez vers le code SOLID, plus il devient fonctionnel . Tôt ou tard, vous pouvez aussi franchir le pas. L'architecture fonctionnelle est l'architecture des ports et des adaptateurs , et l' injection de dépendance est également une tentative, ainsi que des ports et des adaptateurs . La différence, cependant, est qu'un langage comme Haskell applique cette architecture via son système de types.

Favoriser la programmation fonctionnelle typée statiquement

À ce stade, j’ai essentiellement renoncé à la programmation orientée objet (POO), même si de nombreux problèmes sont intrinsèquement liés aux langages classiques tels que Java et C #, plus que le concept lui-même.

Le problème des langages POO classiques est qu’il est presque impossible d’éviter le problème de l’explosion combinatoire, qui, non testé, conduit à des exceptions d’exécution. Les langages à typage statique tels que Haskell et F # permettent quant à eux de coder de nombreux points de décision dans le système de types. Cela signifie qu'au lieu de devoir écrire des milliers de tests, le compilateur vous dira simplement si vous avez traité tous les chemins de code possibles (dans une certaine mesure; ce n'est pas une solution miracle).

En outre, l' injection de dépendance n'est pas fonctionnelle . La vraie programmation fonctionnelle doit rejeter toute la notion de dépendance . Le résultat est un code plus simple.

Sommaire

Si je suis obligé de travailler avec C #, je préfère l’injection de dépendances à granularité fine, car elle me permet de couvrir toute la base du code avec un nombre raisonnable de cas de test.

En fin de compte, ma motivation est un retour rapide. Néanmoins, les tests unitaires ne sont pas le seul moyen d'obtenir des commentaires .

Mark Seemann
la source
1
J'aime beaucoup cette réponse, et en particulier cette déclaration utilisée dans Spring: "La configuration des dépendances à l'aide de XML est le pire des deux mondes. D'abord, vous perdez la sécurité des types de compilation, mais vous ne gagnez rien. Une configuration XML Le fichier peut facilement être aussi gros que le code qu’il essaie de remplacer. "
Thomas Carlisle
@ThomasCarlisle n'était pas le point de la configuration XML pour que vous n'ayez pas à toucher le code (même à le compiler) pour le changer? Je ne l'ai jamais (ou à peine) utilisé à cause des deux choses mentionnées par Mark, mais vous gagnez quelque chose en retour.
El Mac
1

D'énormes classes d'installation de DI sont un problème. Mais pensez au code qu'il remplace.

Vous devez instancier tous ces services et tout le reste. Le code à faire serait soit au app.main()point de départ et injecté manuellement, soit étroitement couplé comme this.myService = new MyService();dans les classes.

Réduisez la taille de votre classe d'installation en la divisant en plusieurs classes d'installation et appelez-les à partir du point de départ du programme. c'est à dire.

main()
{
   var c = new diContainer();
   var service1 = diSetupClass.SetupService1(c);
   var service2 = diSetupClass.SetupService2(c, service1); //if service1 is required by service2
   //etc

   //main logic
}

Vous n'avez pas besoin de faire référence à service1 ou 2 sauf pour les transmettre à d'autres méthodes de configuration ci.

C'est mieux que d'essayer de les extraire du conteneur car cela impose l'ordre d'appeler les configurations.

Ewan
la source
1
Pour ceux qui souhaitent approfondir ce concept (construction de l’arborescence de classe au début du programme), le meilleur terme à rechercher est "composition root"
e_i_pi le
@Ewan, quelle est cette civariable?
Superjos
votre classe d'installation ci avec des méthodes statiques
Ewan
urg devrait être di. Je continue à penser 'conteneur'
Ewan