J'essaie d'introduire DI en tant que modèle ici au travail et l'un de nos principaux développeurs aimerait savoir: quels sont, le cas échéant, les inconvénients utiliser le modèle d'injection de dépendance?
Notez que je cherche ici une liste - si possible - exhaustive, et non une discussion subjective sur le sujet.
Clarification : Je parle du modèle d'injection de dépendance (voir cet article de Martin Fowler), n'est pas un framework spécifique, qu'il soit basé sur XML (comme Spring), sur un code (tel que Guice) ou "autodécoupé".
Edit : Une excellente discussion/discussion/débat en cours / r/programmation ici.
Quelques points:
Généralement, l'avantage du découplage facilite la lecture et la compréhension de chaque tâche, mais augmente la complexité de l'orchestration des tâches plus complexes.
Le même problème de base que vous rencontrez souvent avec la programmation orientée objet, les règles de style et à peu près tout le reste. Il est possible - très courant en fait - de faire trop d’abstractions, d’ajouter trop d’indirection et d’appliquer de manière générale les bonnes techniques de manière excessive et au mauvais endroit.
Chaque motif ou autre construction que vous appliquez apporte de la complexité. L'abstraction et l'indirection dispersent les informations, déplaçant parfois des détails non pertinents, mais rendant parfois plus difficile la compréhension de ce qui se passe exactement. Chaque règle que vous appliquez apporte une rigidité, excluant les options qui pourraient bien être la meilleure approche.
Le but est d'écrire du code qui fait le travail, qui soit robuste, lisible et maintenable. Vous êtes un développeur de logiciels et non un constructeur de tours d'ivoire.
Liens pertinents
http://thedailywtf.com/Articles/The_Inner-Platform_Effect.aspx
http://www.joelonsoftware.com/articles/fog0000000018.html
La forme la plus simple d'injection de dépendance (ne riez pas) est probablement un paramètre. Le code dépendant dépend des données, et ces données sont injectées par le moyen de transmettre le paramètre.
Oui, c'est idiot et cela ne traite pas du point d'injection de dépendance orienté objet, mais un programmeur fonctionnel vous dira que (si vous avez des fonctions de première classe), c'est le seul type d'injection de dépendance dont vous avez besoin. Le point ici est de prendre un exemple trivial et de montrer les problèmes potentiels.
Prenons cette simple fonction traditionnelle - la syntaxe C++ n’est pas significative ici, mais je dois l’épeler d’une manière ou d’une autre ...
void Say_Hello_World ()
{
std::cout << "Hello World" << std::endl;
}
J'ai une dépendance que je veux extraire et injecter - le texte "Hello World". Assez facile...
void Say_Something (const char *p_text)
{
std::cout << p_text << std::endl;
}
Comment est-ce plus inflexible que l'original? Eh bien, si je décidais que la sortie devrait être unicode. Je veux probablement passer de std :: cout à std :: wcout. Mais cela signifie que mes chaînes doivent alors être de type wchar_t, pas de caractère. Chaque appelant doit être changé ou (plus raisonnablement), l'ancienne implémentation est remplacée par un adaptateur qui traduit la chaîne et appelle la nouvelle implémentation.
C’est là un travail de maintenance qui ne serait pas nécessaire si nous avions conservé l’original.
Et si cela semble trivial, jetez un coup d'œil à cette fonction réelle de l'API Win32 ...
http://msdn.Microsoft.com/en-us/library/ms632680%28v=vs.85%29.aspx
C'est 12 "dépendances" à traiter. Par exemple, si les résolutions d'écran deviennent vraiment énormes, nous aurons peut-être besoin de valeurs de coordonnées 64 bits et d'une autre version de CreateWindowEx. Et oui, il y a déjà une ancienne version qui traîne toujours, qui est probablement mappée à la nouvelle version en coulisse ...
http://msdn.Microsoft.com/en-us/library/ms632679%28v=vs.85%29.aspx
Ces "dépendances" ne sont pas un problème pour le développeur d'origine. Tous ceux qui utilisent cette interface doivent rechercher quelles sont ces dépendances, comment elles sont spécifiées et ce qu'elles veulent dire, et déterminer ce qu'elles doivent faire pour leur application. C’est là que les mots "défauts raisonnables" peuvent rendre la vie beaucoup plus simple.
L'injection de dépendance orientée objet n'est pas différente en principe. L'écriture d'une classe est une surcharge, à la fois en texte de code source et en temps de développeur, et si cette classe est écrite pour fournir des dépendances selon certaines spécifications d'objet dépendant, l'objet dépendant est alors verrouillé pour prendre en charge cette interface, même si le besoin s'en fait sentir. pour remplacer la mise en œuvre de cet objet.
Rien de tout cela ne doit être interprété comme prétendant que l’injection de dépendance est mauvaise, loin de là. Mais toute bonne technique peut être appliquée de manière excessive et au mauvais endroit. De même que toutes les chaînes ne doivent pas nécessairement être extraites et transformées en paramètres, tous les comportements de bas niveau ne doivent pas être extraits d’objets de haut niveau et transformés en dépendance injectable.
Voici ma propre réaction initiale: Fondamentalement, les mêmes inconvénients de tout modèle.
Le plus gros inconvénient d'Inversion of Control (pas tout à fait DI, mais assez proche) est que cela a tendance à supprimer le fait d'avoir un seul point pour regarder un aperçu d'un algorithme. C'est essentiellement ce qui se produit lorsque vous avez du code découplé - la possibilité de regarder à un endroit donné est un artefact de couplage étroit.
Je ne pense pas qu'une telle liste existe, cependant essayez de lire ces articles:
J'utilise Guice (framework Java DI) de manière intensive depuis 6 mois. Bien que dans l’ensemble, j’estime que c’est génial (surtout du point de vue des tests), il ya certains inconvénients. Notamment:
Maintenant que je me suis plaint. Permettez-moi de dire que je continuerai (volontiers) à utiliser Guice dans mon projet actuel et très probablement mon prochain. L'injection de dépendance est un modèle génial et incroyablement puissant. Mais cela peut certainement être déroutant et vous passerez presque certainement du temps à maudire quel que soit le cadre d'injection de dépendance que vous choisissez.
De plus, je suis d’accord avec d’autres affiches pour dire que l’injection de dépendance peut être surutilisée.
Le code sans DI comporte le risque bien connu de se perdre dans code Spaghetti - certains symptômes sont que les classes et les méthodes sont trop grandes, en font trop et ne peuvent pas être facilement modifiées, décomposées, refactorisées , ou testé.
Le code avec DI utilisé beaucoup peut être code Ravioli où chaque petite classe est comme une pépite de ravioli individuelle - il fait une petite chose et le principe de responsabilité unique est respecté, auquel est bon. Mais en regardant les classes seules, il est difficile de voir ce que fait le système dans son ensemble, car cela dépend de la manière dont toutes ces petites parties s'emboîtent, ce qui est difficile à voir. Cela ressemble à un gros tas de petites choses.
En évitant la complexité spaghetti des gros morceaux de code couplé au sein d'une grande classe, vous courez le risque d'un autre type de complexité, où il y a beaucoup de petites classes simples et où les interactions entre elles sont complexes.
Je ne pense pas que ce soit un inconvénient fatal - DI vaut toujours la peine. Un certain degré de style de raviolis avec de petites classes ne faisant qu'une chose est probablement bon. Même en excès, je ne pense pas que ce soit mauvais en tant que code spaghetti. Mais être conscient que cela peut aller trop loin est la première étape pour l'éviter. Suivez les liens pour discuter de la façon de l'éviter.
Si vous avez une solution maison, les dépendances sont dans votre visage dans le constructeur. Ou peut-être comme paramètres de méthode qui, là encore, n’est pas trop difficile à repérer. Bien que les dépendances gérées par le framework, si poussées à l'extrême, puissent commencer à apparaître comme par magie.
Cependant, avoir trop de dépendances dans trop de classes est un signe clair que la structure de votre classe est foutue. Ainsi, d’une manière ou d’une autre, l’injection de dépendance (développée en interne ou gérée par une structure) peut aider à résoudre des problèmes de conception criants qui pourraient sinon être cachés cachés dans le noir.
Pour mieux illustrer le deuxième point, voici un extrait de cet article article ( source originale ) qui, à mon avis, est le problème fondamental de la construction de tout système, pas uniquement informatique.
Supposons que vous vouliez concevoir un campus universitaire. Vous devez déléguer une partie de la conception aux étudiants et aux professeurs, sinon le bâtiment de physique ne fonctionnera pas bien pour les spécialistes de la physique. Aucun architecte n'en sait assez sur ce dont les gens de physique ont besoin pour tout faire eux-mêmes. Mais vous ne pouvez pas déléguer la conception de chaque pièce à ses occupants, car vous aurez alors un tas de gravats géant.
Comment répartir la responsabilité de la conception à tous les niveaux d'une grande hiérarchie, tout en maintenant la cohérence et l'harmonie de la conception générale? C'est le problème de conception architecturale que tente de résoudre Alexander, mais c'est aussi un problème fondamental du développement de systèmes informatiques.
Est-ce que DI résout ce problème? Non . Mais cela vous aide à voir clairement si vous essayez de déléguer la responsabilité de concevoir chaque pièce à ses occupants.
Une chose qui me fait hésiter un peu avec DI est l’hypothèse que tous les objets injectés sont peu coûteux à instancier et produisent pas d'effets secondaires -OU- la dépendance est utilisée si souvent qu'elle dépasse tous les coûts d'instanciation associés.
Ceci peut être significatif lorsqu'une dépendance n'est pas fréquemment utilisée dans une classe consommatrice; comme quelque chose comme un IExceptionLogHandlerService
. De toute évidence, un service comme celui-ci est invoqué (espérons-le :)) rarement au sein de la classe - vraisemblablement uniquement sur les exceptions devant être journalisées; Pourtant, le modèle canonique d'injection de constructeur ...
Public Class MyClass
Private ReadOnly mExLogHandlerService As IExceptionLogHandlerService
Public Sub New(exLogHandlerService As IExceptionLogHandlerService)
Me.mExLogHandlerService = exLogHandlerService
End Sub
...
End Class
... nécessite qu'une instance "en direct" de ce service soit fournie, sacrément les coûts/effets secondaires nécessaires pour l'obtenir. Cela ne serait probablement pas le cas, mais que se passerait-il si la construction de cette instance de dépendance impliquait un hit service/base de données, ou une recherche de fichier de configuration, ou le verrouillage d’une ressource jusqu’à ce qu’elle soit supprimée? Si ce service a plutôt été construit selon les besoins, situé sur le service ou généré en usine (tous ayant des problèmes qui leur sont propres), vous ne prendrez alors le coût de la construction que lorsque cela sera nécessaire.
Maintenant, c’est un principe de conception de logiciel généralement accepté que construire un objet est économique et que ne le fait pas produit des effets secondaires. Et bien que ce soit une bonne idée, ce n'est pas toujours le cas. L'utilisation de l'injection de constructeur typique exige cependant fondamentalement que ce soit le cas. Cela signifie que lorsque vous créez une implémentation d'une dépendance, vous devez la concevoir en pensant à DI. Vous auriez peut-être rendu la construction d'objet plus coûteuse pour obtenir des avantages ailleurs, mais si cette implémentation doit être injectée, cela vous obligera probablement à reconsidérer cette conception.
Par ailleurs, certaines techniques peuvent atténuer ce problème en permettant le chargement paresseux des dépendances injectées, par exemple. fournissant à une classe une instance Lazy<IService>
en tant que dépendance. Cela modifierait les constructeurs de vos objets dépendants et rendrait alors encore plus conscients des détails d'implémentation tels que les dépenses de construction d'objet, ce qui n'est sans doute pas souhaitable non plus.
L'illusion que vous avez découplé votre code simplement en implémentant l'injection de dépendance sans le découpler ACTUELLEMENT. Je pense que c'est la chose la plus dangereuse à propos de DI.
Ceci est plus d'un nitpick. Mais l’un des inconvénients de l’injection de dépendance est qu’il est un peu plus difficile pour les outils de développement de raisonner et de naviguer dans le code.
Plus précisément, si vous maintenez la touche Ctrl/Commande-Clic enfoncée lors d’un appel de méthode en code, vous accédez à la déclaration de la méthode sur une interface plutôt qu’à une implémentation concrète.
C’est vraiment un inconvénient d’un code faiblement couplé (code conçu par l’interface), qui s’applique même si vous n’utilisez pas l’injection de dépendances (c’est-à-dire, même si vous utilisez simplement des usines). Mais l'avènement de l'injection de dépendance est ce qui a vraiment encouragé le code à couplage lâche aux masses, alors j'ai pensé que je le mentionnerais.
En outre, les avantages d'un code faiblement couplé l'emportent largement sur ce point, je l'appelle donc un nitpick. Bien que j'ai travaillé assez longtemps pour savoir que c'est le genre de refoulement que vous pourriez avoir si vous tentiez d'introduire une injection de dépendance.
En fait, je m'aventurerais à deviner que pour chaque "inconvénient" que vous pouvez trouver avec l'injection de dépendance, vous trouverez de nombreux avantages qui l'emportent de loin sur elle.
dépendante du constructeur L’injection de dépendance (sans l’aide de "cadres" magiques) est un moyen simple et avantageux de structurer le code OO. Dans les meilleures bases de code que j'ai vues, au fil des années passées avec d'autres anciens collègues de Martin Fowler, j'ai commencé à remarquer que la plupart des bonnes classes écrites de cette façon finissaient par avoir une seule méthode doSomething
.
L'inconvénient majeur est donc qu'une fois que vous vous rendez compte que vous n'utilisez plus qu'une méthode compliquée OO pour écrire des fermetures en tant que cours afin d'obtenir les avantages de la programmation fonctionnelle, votre motivation pour écrire OO le code peut s'évaporer rapidement.
Je trouve que l'injection de constructeur peut conduire à de gros constructeurs laids (et je l'utilise dans tout mon code - peut-être que mes objets sont trop granulaires?). De plus, parfois, avec l'injection de constructeur, je finis par avoir d'horribles dépendances circulaires (bien que cela soit très rare), de sorte que vous pouvez vous retrouver dans une sorte de cycle de vie prêt à fonctionner avec plusieurs cycles d'injection de dépendance dans un système plus complexe.
Cependant, je suis favorable à l’injection du construteur à l’injection du setter, car une fois mon objet construit, je sais sans aucun doute dans quel état il se trouve, qu’il soit dans un environnement de test unitaire ou chargé dans un conteneur IOC. Ce qui, en quelque sorte, est ce que je ressens comme le principal inconvénient de l’injection de setter.
(en tant que note de bas de page, je trouve le sujet entier assez "religieux", mais votre kilométrage variera en fonction du niveau de fanatisme technique de votre équipe de développeurs!)
Si vous utilisez DI sans IOC conteneur, le plus gros inconvénient est que vous voyez rapidement le nombre de dépendances de votre code et le niveau de couplage. ("Mais je pensais que c'était un bon design!") La progression naturelle est d'aller vers un conteneur IOC qui peut prendre un peu de temps à apprendre et à mettre en œuvre (beaucoup moins mauvais que l'apprentissage WPF courbe, mais ce n’est pas gratuit non plus). Le dernier inconvénient est que certains développeurs vont commencer à écrire des tests unitaires honnêtes et probants et qu'il leur faudra du temps pour le comprendre. Les développeurs qui pouvaient déjà créer quelque chose en une demi-journée vont passer deux jours à essayer de comprendre comment se moquer de toutes leurs dépendances.
Semblable à la réponse de Mark Seemann, le résultat final est que vous passez du temps à devenir un meilleur développeur plutôt que de pirater des morceaux de code ensemble et de les lancer en production. Quelle serait votre entreprise plutôt? Vous seul pouvez y répondre.
DI est une technique ou un motif et n'est lié à aucun cadre. Vous pouvez câbler vos dépendances manuellement. DI vous aide en matière de RS (responsabilité unique) et de SoC (séparation des préoccupations). DI conduit à une meilleure conception. De mon point de vue et de mon expérience il n’ya aucun inconvénient. Comme avec tout autre schéma, vous pouvez vous tromper ou vous en abuser (mais ce qui est assez difficile dans le cas de DI).
Si vous introduisez DI en principe dans une application existante, utilisez un cadre - la plus grande erreur que vous puissiez faire est de l'utiliser à mauvais escient en tant que localisateur de service. Le framework DI + lui-même est génial et n'a fait que rendre les choses meilleures partout où je l'ai vu! Du point de vue organisationnel, il existe des problèmes communs à chaque nouveau processus, technique, modèle, ...:
En général, vous devez investir du temps et de l'argent, à côté de cela, il n'y a aucun inconvénient, vraiment!
Lisibilité du code. Vous ne pourrez pas facilement comprendre le flux de code car les dépendances sont cachées dans des fichiers XML.
Il semble que les avantages supposés d'un langage à typage statique diminuent de manière significative lorsque vous utilisez constamment des techniques pour contourner le typage statique. Un grand magasin Java que je venais d'interviewer mettait en correspondance leurs dépendances de construction avec l'analyse de code statique ... qui devait analyser tous les fichiers Spring pour être efficace.
Deux choses:
Par exemple, IntelliJ (édition commerciale) prend en charge la vérification de la validité d'une configuration Spring et marque les erreurs telles que les violations de type dans la configuration. Sans ce type de prise en charge, vous ne pouvez pas vérifier si la configuration est valide avant d'exécuter des tests.
C'est l'une des raisons pour lesquelles le motif 'cake' (comme il est connu dans la communauté Scala) est une bonne idée: le câblage entre les composants peut être vérifié par le vérificateur de types. Vous n'avez pas cet avantage avec les annotations ou XML.
Des structures telles que Spring ou Guice rendent difficile la détermination statique de l'apparence du graphe d'objets créé par le conteneur. Bien qu'ils créent un graphe d'objet au démarrage du conteneur, ils ne fournissent pas d'API utiles décrivant le graphe d'objet qui serait/serait/serait créé.
Cela peut augmenter le temps de démarrage de l'application car le conteneur IoC doit résoudre les dépendances de manière appropriée et nécessite parfois plusieurs itérations.