Les frameworks d'injection de dépendances comme Google Guice donnent la motivation suivante pour leur utilisation ( source ):
Pour construire un objet, vous construisez d'abord ses dépendances. Mais pour construire chaque dépendance, vous avez besoin de ses dépendances, etc. Ainsi, lorsque vous créez un objet, vous devez vraiment créer un graphique d'objet.
Construire des graphiques d'objets à la main demande beaucoup de travail (...) et rend les tests difficiles.
Mais je n'achète pas cet argument: même sans framework d'injection de dépendances, je peux écrire des classes qui sont à la fois faciles à instancier et pratiques à tester. Par exemple, l'exemple de la page de motivation de Guice pourrait être réécrit de la manière suivante:
class BillingService
{
private final CreditCardProcessor processor;
private final TransactionLog transactionLog;
// constructor for tests, taking all collaborators as parameters
BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
{
this.processor = processor;
this.transactionLog = transactionLog;
}
// constructor for production, calling the (productive) constructors of the collaborators
public BillingService()
{
this(new PaypalCreditCardProcessor(), new DatabaseTransactionLog());
}
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard)
{
...
}
}
Il peut donc y avoir d'autres arguments pour les cadres d'injection de dépendances ( qui sont hors de portée pour cette question !), Mais la création facile de graphiques d'objets testables n'en fait pas partie, n'est-ce pas?
new ShippingService()
.Réponses:
Il y a un vieux, vieux débat en cours sur la meilleure façon de faire l'injection de dépendance.
La coupe originale de Spring a instancié un objet simple, puis injecté des dépendances via des méthodes de définition.
Mais alors, une grande contingence de personnes a insisté sur le fait que l'injection de dépendances via les paramètres du constructeur était la bonne façon de le faire.
Puis, ces derniers temps, alors que l'utilisation de la réflexion est devenue plus courante, la fixation directe des valeurs des membres privés, sans arguments ni constructeurs, est devenue à la mode.
Votre premier constructeur est donc cohérent avec la deuxième approche de l'injection de dépendances. Il vous permet de faire de belles choses comme injecter des simulations pour les tests.
Mais le constructeur sans argument a ce problème. Puisqu'il instancie les classes d' implémentation pour
PaypalCreditCardProcessor
etDatabaseTransactionLog
, il crée une dépendance difficile et à la compilation avec PayPal et la base de données. Il prend la responsabilité de construire et de configurer correctement tout cet arbre de dépendances.Imaginez que le processeur PayPay est un sous-système vraiment compliqué et tire en outre de nombreuses bibliothèques de support. En créant une dépendance au moment de la compilation sur cette classe d'implémentation, vous créez un lien incassable vers cet arbre de dépendance entier. La complexité de votre graphique d'objet vient de bondir d'un ordre de grandeur, peut-être deux.
Un grand nombre de ces éléments dans l'arborescence des dépendances seront transparents, mais beaucoup d'entre eux devront également être instanciés. Il y a de fortes chances que vous ne puissiez pas simplement instancier a
PaypalCreditCardProcessor
.En plus de l'instanciation, chacun des objets aura besoin de propriétés appliquées à partir de la configuration.
Si vous n'avez qu'une dépendance sur l'interface, et autorisez une fabrique externe à construire et à injecter la dépendance, ils vous coupent tout l'arborescence des dépendances PayPal, et la complexité de votre code s'arrête à l'interface.
Il y a d'autres avantages, comme la spécification des classes d'implémentation dans la configuration (c'est-à-dire au moment de l'exécution plutôt que lors de la compilation), ou la spécification de dépendances plus dynamique qui varie, par exemple, selon l'environnement (test, intégration, production).
Par exemple, disons que PayPalProcessor avait 3 objets dépendants, et chacune de ces dépendances en avait deux autres. Et tous ces objets doivent extraire les propriétés de la configuration. Le code en l'état assumerait la responsabilité de construire tout cela, de définir les propriétés à partir de la configuration, etc. etc. - toutes les préoccupations dont le cadre DI prendra soin.
Il peut ne pas sembler évident au premier abord de quoi vous vous protégez en utilisant un cadre DI, mais il s'additionne et devient douloureusement évident au fil du temps. (lol je parle de l'expérience d'avoir essayé de le faire à la dure)
...
Dans la pratique, même pour un programme vraiment minuscule, je trouve que je finis par écrire dans un style DI, et divise les classes en paires implémentation / usine. Autrement dit, si je n'utilise pas un framework DI comme Spring, je jette juste ensemble quelques classes d'usine simples.
Cela permet de séparer les préoccupations afin que ma classe puisse tout faire, et la classe d'usine assume la responsabilité de construire et de configurer les choses.
Pas une approche requise, mais FWIW
...
Plus généralement, le modèle DI / interface réduit la complexité de votre code en faisant deux choses:
abstraction des dépendances en aval dans les interfaces
«lever» les dépendances en amont hors de votre code et dans une sorte de conteneur
En plus de cela, puisque l'instanciation et la configuration d'objets sont une tâche assez familière, le framework DI peut réaliser de nombreuses économies d'échelle grâce à une notation standardisée et à des astuces comme la réflexion. La diffusion de ces mêmes préoccupations autour des cours finit par ajouter beaucoup plus d'encombrement qu'on ne le pense.
la source
En liant votre graphe d'objet au début du processus, c'est-à-dire en le câblant dans le code, vous avez besoin que le contrat et l'implémentation soient présents. Si quelqu'un d'autre (peut-être même vous) veut utiliser ce code dans un but légèrement différent, il doit recalculer le graphique d'objet entier et le réimplémenter.
Les frameworks DI vous permettent de prendre un tas de composants et de les connecter ensemble lors de l'exécution. Cela rend le système "modulaire", composé d'un certain nombre de modules qui fonctionnent sur les interfaces des uns et des autres plutôt que sur les implémentations des uns et des autres.
la source
L'approche «instancier mes propres collaborateurs» peut fonctionner pour les arbres de dépendance , mais elle ne fonctionnera certainement pas bien pour les graphiques de dépendance qui sont des graphiques acycliques dirigés généraux (DAG). Dans un DAG de dépendance, plusieurs nœuds peuvent pointer vers le même nœud, ce qui signifie que deux objets utilisent le même objet comme collaborateur. Ce cas ne peut en effet pas être construit avec l'approche décrite dans la question.
Si certains de mes collaborateurs (ou collaborateurs du collaborateur) devaient partager un certain objet, je devrais instancier cet objet et le transmettre à mes collaborateurs. Donc, en fait, j'aurais besoin d'en savoir plus que mes collaborateurs directs, et cela n'est évidemment pas à l'échelle.
la source
UnitOfWork
qui a unDbContext
référentiel unique et multiple. Tous ces référentiels doivent utiliser le mêmeDbContext
objet. Avec «l'auto-instanciation» proposée par OP, cela devient impossible à faire.Je n'ai pas utilisé Google Guice, mais j'ai pris beaucoup de temps à migrer d'anciennes applications N-tier héritées dans .Net vers des architectures IoC comme Onion Layer qui dépendent de l'injection de dépendance pour découpler les choses.
Pourquoi l'injection de dépendance?
Le but de l'injection de dépendance n'est pas réellement pour la testabilité, c'est en fait de prendre des applications étroitement couplées et de desserrer le couplage autant que possible. (Ce qui a pour sous-produit souhaitable de rendre votre code beaucoup plus facile à adapter pour des tests unitaires appropriés)
Pourquoi devrais-je m'inquiéter du couplage?
Le couplage ou les dépendances étroites peuvent être une chose très dangereuse. (Surtout dans les langues compilées) Dans ces cas, vous pourriez avoir une bibliothèque, une DLL, etc. qui est très rarement utilisée et qui a un problème qui met efficacement l'application entière hors ligne. (Votre application entière meurt parce qu'une pièce sans importance a un problème ... c'est mauvais ... VRAIMENT mauvais) Maintenant, lorsque vous dissociez des éléments, vous pouvez réellement configurer votre application afin qu'elle puisse s'exécuter même si cette DLL ou cette bibliothèque est entièrement manquante! Bien sûr, une seule pièce qui a besoin de cette bibliothèque ou DLL ne fonctionnera pas, mais le reste de l'application est aussi heureux que possible.
Pourquoi ai-je besoin d'une injection de dépendance pour effectuer des tests appropriés
Vraiment, vous voulez juste du code à couplage lâche, l'injection de dépendances le permet. Vous pouvez coupler librement des choses sans IoC, mais généralement c'est plus de travail et moins adaptable (je suis sûr que quelqu'un a une exception)
Dans le cas que vous avez donné, je pense qu'il serait beaucoup plus facile de simplement configurer l'injection de dépendance afin que je puisse me moquer du code que je ne suis pas intéressé à compter dans le cadre de ce test. Dites-nous simplement la méthode "Hé, je sais que je vous ai dit d'appeler le référentiel, mais à la place, voici les données qu'il" devrait "renvoyer des clins d'œil " maintenant parce que ces données ne changent jamais, vous savez que vous testez uniquement la partie qui utilise ces données, pas la récupération effective des données.
Fondamentalement, lorsque vous testez, vous voulez des tests d'intégration (fonctionnalité) qui testent un élément de fonctionnalité du début à la fin, et des tests unitaires complets qui testent chaque élément de code (généralement au niveau de la méthode ou de la fonction) indépendamment.
L'idée est que vous voulez vous assurer que toute la fonctionnalité fonctionne, sinon vous voulez connaître la partie exacte du code qui ne fonctionne pas.
Cette CAN se faire sans injection de dépendance, mais le plus souvent que votre projet se développe , il devient de plus en plus lourd à le faire sans injection de dépendances en place. (TOUJOURS supposer que votre projet va grandir! Il vaut mieux avoir une pratique inutile de compétences utiles que de trouver un projet qui monte rapidement et nécessite une refactorisation et une réingénierie sérieuses après que les choses décollent déjà.)
la source
Comme je le mentionne dans une autre réponse , le problème ici est que vous voulez que la classe
A
dépende d' une classeB
sans coder en dur quelle classeB
est utilisée dans le code source de A. C'est impossible en Java et en C # car la seule façon d'importer une classe est de s'y référer par un nom unique au monde.En utilisant une interface, vous pouvez contourner la dépendance de classe codée en dur, mais vous devez toujours mettre la main sur une instance de l'interface, et vous ne pouvez pas appeler de constructeurs ou vous êtes de retour au carré 1. Alors maintenant, codez qui pourrait autrement créer ses dépendances repousse cette responsabilité envers quelqu'un d'autre. Et ses dépendances font la même chose. Alors maintenant, chaque fois que vous avez besoin d'une instance d'une classe, vous finissez par construire manuellement l'arborescence de dépendances entière, alors que dans le cas où la classe A dépend directement de B, vous pouvez simplement appeler
new A()
et avoir cet appel constructeurnew B()
, et ainsi de suite.Un framework d'injection de dépendances tente de contourner cela en vous permettant de spécifier les mappages entre les classes et de construire l'arborescence de dépendances pour vous. Le problème est que lorsque vous bousiller les mappages, vous découvrirez au moment de l'exécution, pas au moment de la compilation comme vous le feriez dans les langages qui prennent en charge les modules de mappage en tant que concept de première classe.
la source
Je pense que c'est un gros malentendu ici.
Guice est un framework d' injection de dépendances . Cela rend DI automatique . Le point qu'ils ont soulevé dans l'extrait que vous avez cité concerne le fait que Guice puisse supprimer le besoin de créer manuellement le "constructeur testable" que vous avez présenté dans votre exemple. Cela n'a absolument rien à voir avec l'injection de dépendance elle-même.
Ce constructeur:
utilise déjà l'injection de dépendance. Vous venez de dire que l'utilisation de la DI est facile.
Le problème que Guice résout est que pour utiliser ce constructeur, vous devez maintenant avoir un code de constructeur de graphique d'objet quelque part , en passant manuellement les objets déjà instanciés comme arguments de ce constructeur. Guice vous permet d'avoir un seul endroit où vous pouvez configurer quelles classes d'implémentation réelles correspondent à celles-ci
CreditCardProcessor
et auxTransactionLog
interfaces. Après cette configuration, chaque fois que vous créez à l'BillingService
aide de Guice, ces classes sont transmises automatiquement au constructeur.C'est ce que fait le cadre d' injection de dépendance . Mais le constructeur lui-même que vous avez présenté est déjà une implémentation du principe d' injection de dépendance . Les conteneurs IoC et les frameworks DI sont des moyens d'automatiser les principes correspondants mais rien ne vous empêche de tout faire à la main, c'était tout.
la source