Injection de dépendances / pratiques de conteneur IoC lors de l'écriture de frameworks

18

J'ai utilisé divers conteneurs IoC (Castle.Windsor, Autofac, MEF, etc.) pour .Net dans un certain nombre de projets. J'ai constaté qu'ils ont tendance à être fréquemment maltraités et à encourager un certain nombre de mauvaises pratiques.

Existe-t-il des pratiques établies pour l'utilisation des conteneurs IoC, en particulier lors de la fourniture d'une plate-forme / d'un framework? Mon objectif en tant que rédacteur de framework est de rendre le code aussi simple et aussi facile à utiliser que possible. Je préfère écrire une ligne de code pour construire un objet que dix ou même seulement deux.

Par exemple, quelques odeurs de code que j'ai remarquées et qui n'ont pas de bonnes suggestions pour:

  1. Grand nombre de paramètres (> 5) pour les constructeurs. La création de services a tendance à être complexe; toutes les dépendances sont injectées via le constructeur - malgré le fait que les composants sont rarement optionnels (sauf peut-être en test).

  2. Manque de cours privés et internes; celui-ci peut être une limitation spécifique de l'utilisation de C # et Silverlight, mais je suis intéressé par la façon dont il est résolu. Il est difficile de dire ce qu'est une interface de frameworks si toutes les classes sont publiques; cela me permet d'accéder à des parties privées que je ne devrais probablement pas toucher.

  3. Couplage du cycle de vie de l'objet au conteneur IoC. Il est souvent difficile de construire manuellement les dépendances requises pour créer des objets. Le cycle de vie des objets est trop souvent géré par le cadre IoC. J'ai vu des projets où la plupart des classes sont enregistrées en tant que singletons. Vous obtenez un manque de contrôle explicite et êtes également obligé de gérer les internes (cela concerne le point ci-dessus, toutes les classes sont publiques et vous devez les injecter).

Par exemple, le framework .Net possède de nombreuses méthodes statiques. tels que DateTime.UtcNow. Plusieurs fois, j'ai vu cela enveloppé et injecté comme paramètre de construction.

Selon l'implémentation concrète, mon code est difficile à tester. L'injection d'une dépendance rend mon code difficile à utiliser - en particulier si la classe a de nombreux paramètres.

Comment puis-je fournir à la fois une interface testable et une interface facile à utiliser? Quelles sont les meilleures pratiques?

Dave Hillier
la source
1
Quelles mauvaises pratiques encouragent-ils? Quant aux cours privés, vous ne devriez pas avoir besoin d'en avoir beaucoup si vous avez une bonne encapsulation; d'une part, ils sont beaucoup plus difficiles à tester.
Aaronaught
Euh, 1 et 2 sont les mauvaises pratiques. Grands constructeurs et n'utilisant pas l'encapsulation pour établir une interface minimale? Aussi, j'ai dit privé et interne (utilisez des internes visibles pour, pour tester). Je n'ai jamais utilisé un framework qui m'oblige à utiliser un conteneur IoC particulier.
Dave Hillier
2
Est-il possible qu'un signe de besoin de nombreux paramètres pour vos constructeurs de classe soit lui-même l'odeur de code et que le DIP rend cela plus évident?
dreza
3
L'utilisation d'un conteneur IoC spécifique dans un cadre n'est généralement pas une bonne pratique. L'utilisation de l'injection de dépendance est une bonne pratique. Connaissez-vous la différence?
Aaronaught
1
@Aaronaught (de retour d'entre les morts). L'utilisation de "l'injection de dépendance est une bonne pratique" est fausse. L'injection de dépendance est un outil qui ne doit être utilisé que lorsque cela est approprié. Utiliser l'injection de dépendance tout le temps est une mauvaise pratique.
Dave Hillier

Réponses:

27

Le seul anti-modèle d'injection de dépendance légitime que je connaisse est le modèle Service Locator , qui est un anti-modèle lorsqu'un cadre DI est utilisé pour cela.

Tous les autres anti-modèles dits DI dont j'ai entendu parler, ici ou ailleurs, ne sont que des cas légèrement plus spécifiques d'anti-modèles généraux de conception OO / logiciel. Par exemple:

  • La sur-injection du constructeur est une violation du principe de responsabilité unique . Trop d'arguments constructeurs indiquent trop de dépendances; trop de dépendances indique que la classe essaie d'en faire trop. Habituellement, cette erreur est en corrélation avec d'autres odeurs de code, telles que des noms de classe inhabituellement longs ou ambigus ("manager"). Les outils d'analyse statique peuvent facilement détecter un couplage afférent / efférent excessif.

  • L'injection de données, par opposition au comportement, est un sous-type de l' anti-modèle poltergeist , le «geist dans ce cas étant le conteneur. Si une classe doit connaître la date et l'heure actuelles, vous n'injectez pas a DateTime, qui est une donnée; à la place, vous injectez une abstraction sur l'horloge système (j'appelle généralement la mienne ISystemClock, bien que je pense qu'il y en a une plus générale dans le projet SystemWrappers ). Ce n'est pas seulement correct pour DI; il est absolument essentiel pour la testabilité, afin que vous puissiez tester des fonctions variant dans le temps sans avoir à les attendre.

  • Déclarer chaque cycle de vie comme Singleton est, pour moi, un parfait exemple de programmation culte du fret et, dans une moindre mesure, du " cloaque d'objet ", familièrement appelé . J'ai vu plus d'abus de singleton que je ne m'en souviens, et très peu d'entre eux impliquent DI.

  • Une autre erreur courante concerne les types d'interfaces spécifiques à l'implémentation (avec des noms étranges comme IOracleRepository) faits juste pour pouvoir l'enregistrer dans le conteneur. Ceci est en soi une violation du principe d'inversion de dépendance (juste parce que c'est une interface, ne signifie pas qu'elle est vraiment abstraite) et inclut souvent également un ballonnement d'interface qui viole le principe de ségrégation d'interface .

  • La dernière erreur que je vois habituellement est la "dépendance facultative", ce qu'ils ont fait dans NerdDinner . En d'autres termes, il existe un constructeur qui accepte l'injection de dépendances, mais aussi un autre constructeur qui utilise une implémentation "par défaut". Cela viole également le DIP et a tendance à entraîner également des violations de LSP , car les développeurs, au fil du temps, commencent à faire des hypothèses sur l'implémentation par défaut et / ou à démarrer de nouvelles instances à l'aide du constructeur par défaut.

Comme le dit le vieil adage, vous pouvez écrire FORTRAN dans n'importe quelle langue . L' injection de dépendance n'est pas une balle d'argent qui empêche les développeurs de vissage leur gestion de la dépendance, mais il ne prévenir un certain nombre d'erreurs / anti-modèles communs:

...etc.

De toute évidence, vous ne voulez pas concevoir un framework dépendant d'une implémentation de conteneur IoC spécifique , comme Unity ou AutoFac. C'est, encore une fois, violer le DIP. Mais si vous pensez même à faire quelque chose comme ça, vous devez déjà avoir fait plusieurs erreurs de conception, car l'injection de dépendance est une technique de gestion des dépendances à usage général et n'est pas liée au concept d'un conteneur IoC.

Tout peut construire un arbre de dépendance; c'est peut-être un conteneur IoC, c'est peut-être un test unitaire avec un tas de simulations, c'est peut-être un pilote de test fournissant des données factices. Votre framework ne devrait pas s'en soucier, et la plupart des frameworks que j'ai vus ne s'en soucient pas, mais ils utilisent toujours massivement l'injection de dépendance afin qu'elle puisse être facilement intégrée dans le conteneur IoC de l'utilisateur final de choix.

DI n'est pas sorcier. Essayez simplement d'éviter newet staticsauf lorsqu'il existe une raison impérieuse de les utiliser, comme une méthode utilitaire qui n'a pas de dépendances externes, ou une classe utilitaire qui ne pourrait avoir aucun but en dehors du cadre (les enveloppes d'interopérabilité et les clés de dictionnaire sont des exemples courants de cette).

De nombreux problèmes avec les frameworks IoC surviennent lorsque les développeurs apprennent à les utiliser pour la première fois, et au lieu de changer la façon dont ils gèrent les dépendances et les abstractions pour s'adapter au modèle IoC, essayez plutôt de manipuler le conteneur IoC pour répondre aux attentes de leurs ancien style de codage, qui impliquerait souvent un couplage élevé et une faible cohésion. Un mauvais code est un mauvais code, qu'il utilise ou non des techniques DI.

Aaronaught
la source
J'ai quelques questions. Pour une bibliothèque qui est distribuée sur NuGet, je ne peux pas dépendre d'un système IoC comme cela est fait par l'application utilisant la bibliothèque, pas par la bibliothèque elle-même. Dans de nombreux cas, l'utilisateur de la bibliothèque ne se soucie pas du tout de ses dépendances internes. Existe-t-il un problème avec un constructeur par défaut initiant les dépendances internes et un constructeur plus long pour les tests unitaires et / ou les implémentations personnalisées? Vous dites aussi d'éviter les "nouveaux". Est-ce que cela s'applique à des choses comme StringBuilder, DateTime et TimeSpan?
Etienne Charland