Comprendre l'injection de dépendance

109

Je lis sur l' injection de dépendance (DI). Pour moi, c'est une chose très compliquée à faire, car je lisais que c'était aussi une référence à l' inversion de contrôle (IoC) et je me suis dit que j'allais partir en voyage.

C’est ce que je comprends: au lieu de créer un modèle dans la classe qui le consomme également, vous passez (injectez) le modèle (déjà rempli de propriétés intéressantes) à l’endroit où il est nécessaire (à une nouvelle classe qui pourrait le prendre comme paramètre dans le constructeur).

Pour moi, cela ne fait que passer un argument. Je dois avoir mal compris le point? Peut-être que cela devient plus évident avec de plus gros projets?

Ma compréhension est non-DI (en utilisant un pseudo-code):

public void Start()
{
    MyClass class = new MyClass();
}

...

public MyClass()
{
    this.MyInterface = new MyInterface(); 
}

Et DI serait

public void Start()
{
    MyInterface myInterface = new MyInterface();
    MyClass class = new MyClass(myInterface);
}

...

public MyClass(MyInterface myInterface)
{
    this.MyInterface = myInterface; 
}

Quelqu'un pourrait-il faire la lumière, car je suis sûr que je suis embrouillé ici.

MyDaftQuestions
la source
5
Essayez d’avoir 5 de MyInterface et MyClass ET plusieurs classes formant une chaîne de dépendance. Il devient difficile de tout mettre en place.
Euphoric
159
" Pour moi, cela ne fait que passer un argument. J'ai dû mal comprendre le point? " Nope. Tu l'as eu. C'est "l'injection de dépendance". Maintenant, voyez quel autre jargon fou vous pouvez trouver pour des concepts simples, c'est amusant!
Eric Lippert
8
Je ne peux pas voter assez le commentaire de M. Lippert. Moi aussi, j'ai trouvé que le jargon était confus pour quelque chose que je faisais naturellement, de toute façon.
Alex Humphrey
16
@ EricLippert: Bien que correct, je pense que c'est légèrement réducteur. Parameters-as-DI n'est pas un concept simple pour votre programmeur moyen impératif / orienté objet, qui a l'habitude de transmettre des données. Ici, je vous laisse passer le comportement , ce qui nécessite une vision du monde plus flexible que celle d’un programmeur médiocre.
Phoshi
14
@Phoshi: Vous faites valoir un point, mais vous avez également mis le doigt sur pourquoi je suis sceptique quant au fait que «l'injection de dépendance» est une bonne idée en premier lieu. Si j'écris une classe dont la correction et les performances dépendent du comportement d'une autre classe, la dernière chose que je souhaite faire est de charger l'utilisateur de la classe de la construction et de la configuration correctes de la dépendance! C'est mon travail. Chaque fois que vous laissez un consommateur injecter une dépendance, vous créez un point où le consommateur peut le faire mal.
Eric Lippert

Réponses:

106

Eh bien oui, vous injectez vos dépendances, soit par un constructeur, soit par des propriétés.
Une des raisons en est de ne pas encombrer MyClass avec les détails de la manière dont une instance de MyInterface doit être construite. MyInterface pourrait être quelque chose qui contient toute une liste de dépendances et le code de MyClass deviendrait très rapidement moche si vous instanciez toutes les dépendances de MyInterface dans MyClass.

Une autre raison est pour les tests.
Si vous avez une dépendance sur une interface de lecteur de fichiers et que vous l'injectez via un constructeur dans, par exemple, ConsumerClass, cela signifie que lors des tests, vous pouvez passer une implémentation en mémoire du lecteur de fichiers à la ConsumerClass, évitant ainsi la nécessité de: faire I / O pendant les tests.

Stefan Billiet
la source
13
C'est génial, bien expliqué. Veuillez accepter un +1 imaginaire car mon représentant ne le permettra pas encore!
MyDaftQuestions
11
Le scénario de construction compliqué est un bon candidat pour utiliser le modèle Factory. De cette façon, l’usine assume la responsabilité de la construction de l’objet, de sorte que la classe elle-même n’y soit pas obligée et que vous respectiez le principe de responsabilité unique.
Matt Gibson
1
@MattGibson bon point.
Stefan Billiet
2
+1 pour les tests, une DI bien structurée vous aidera à accélérer les tests et à effectuer les tests unitaires contenus dans la classe que vous testez.
Umur Kontacı
52

La mise en œuvre de DI dépend beaucoup du langage utilisé.

Voici un exemple simple non-DI:

class Foo {
    private Bar bar;
    private Qux qux;

    public Foo() {
        bar = new Bar();
        qux = new Qux();
    }
}

Cela est nul, par exemple, pour un test, je veux utiliser un objet fictif bar. Nous pouvons donc rendre cela plus flexible et permettre aux instances d'être transmises via le constructeur:

class Foo {
    private Bar bar;
    private Qux qux;

    public Foo(Bar bar, Qux qux) {
        this.bar = bar;
        this.qux = qux;
    }
}

// in production:
new Foo(new Bar(), new Qux());
// in test:
new Foo(new BarMock(), new Qux());

C'est déjà la forme la plus simple d'injection de dépendance. Mais cela reste quand même gênant parce que tout doit être fait manuellement (également, parce que l'appelant peut détenir une référence à nos objets internes et invalider ainsi notre état).

Nous pouvons introduire plus d'abstraction en utilisant des usines:

  • Une option consiste Fooà générer une usine abstraite.

    interface FooFactory {
        public Foo makeFoo();
    }
    
    class ProductionFooFactory implements FooFactory {
        public Foo makeFoo() { return new Foo(new Bar(), new Baz()) }
    }
    
    class TestFooFactory implements FooFactory {
        public Foo makeFoo() { return new Foo(new BarMock(), new Baz()) }
    }
    
    FooFactory fac = ...; // depends on test or production
    Foo foo = fac.makeFoo();
  • Une autre option consiste à passer une fabrique au constructeur:

    interface DependencyManager {
        public Bar makeBar();
        public Qux makeQux();
    }
    class ProductionDM implements DependencyManager {
        public Bar makeBar() { return new Bar() }
        public Qux makeQux() { return new Qux() }
    }
    class TestDM implements DependencyManager {
        public Bar makeBar() { return new BarMock() }
        public Qux makeQux() { return new Qux() }
    }
    
    class Foo {
        private Bar bar;
        private Qux qux;
    
        public Foo(DependencyManager dm) {
            bar = dm.makeBar();
            qux = dm.makeQux();
        }
    }

Les problèmes restants avec cela sont que nous devons écrire une nouvelle DependencyManagersous-classe pour chaque configuration et que le nombre de dépendances pouvant être gérées est assez limité (chaque nouvelle dépendance nécessite une nouvelle méthode dans l'interface).

Avec des fonctionnalités telles que la réflexion et le chargement de classe dynamique, nous pouvons éviter ce problème. Mais cela dépend beaucoup de la langue utilisée. En Perl, les classes peuvent être référencées par leur nom, et je pourrais simplement faire

package Foo {
    use signatures;

    sub new($class, $dm) {
        return bless {
            bar => $dm->{bar}->new,
            qux => $dm->{qux}->new,
        } => $class;
    }
}

my $prod = { bar => 'My::Bar', qux => 'My::Qux' };
my $test = { bar => 'BarMock', qux => 'QuxMock' };
$test->{bar} = 'OtherBarMock';  # change conf at runtime

my $foo = Foo->new(rand > 0.5 ? $prod : $test);

Dans des langages tels que Java, le gestionnaire de dépendances pourrait se comporter comme un Map<Class, Object>:

Bar bar = dm.make(Bar.class);

La classe à laquelle le problème Bar.classest résolu peut être configurée au moment de l'exécution, par exemple en maintenant une Map<Class, Class>interface qui mappe les interfaces aux implémentations.

Map<Class, Class> dependencies = ...;

public <T> T make(Class<T> c) throws ... {
    // plus a lot more error checking...
    return dependencies.get(c).newInstance();
}

Il y a toujours un élément manuel impliqué dans l'écriture du constructeur. Mais nous pouvons rendre le constructeur complètement inutile, par exemple en pilotant la DI via des annotations:

class Foo {
    @Inject(Bar.class)
    private Bar bar;

    @Inject(Qux.class)
    private Qux qux;

    ...
}

dm.make(Foo.class);  // takes care of initializing "bar" and "qux"

Voici un exemple d'implémentation d'un framework DI minuscule (et très restreint): http://ideone.com/b2ubuF , bien que cette implémentation soit totalement inutilisable pour les objets immuables (cette implémentation naïve ne peut prendre aucun paramètre pour le constructeur).

Amon
la source
7
C'est super avec d'excellents exemples, bien que cela me prenne quelques fois de le lire pour tout digérer, mais merci d'avoir pris autant de temps.
MyDaftQuestions
33
Il est amusant de voir que chaque nouvelle simplification est plus complexe
Kromster,
48

Nos amis de Stack Overflow ont une bonne réponse à cela . Mon préféré est la deuxième réponse, y compris la citation:

"Injection de dépendance" est un terme de 25 dollars pour un concept de 5 cents. (...) L'injection de dépendance consiste à donner à un objet ses variables d'instance. (...)

du blog de James Shore . Bien sûr, il existe des versions / modèles plus complexes qui se superposent, mais cela suffit pour comprendre fondamentalement ce qui se passe. Au lieu qu'un objet crée ses propres variables d'instance, elles sont transmises de l'extérieur.

utilisateur967543
la source
10
J'avais lu ceci ... et jusqu'à présent, cela n'avait aucun sens ... Maintenant, c'est logique. La dépendance n’est pas vraiment un concept à comprendre (quelle que soit sa puissance), mais elle a un grand nom et beaucoup de bruit Internet qui y est associé!
MyDaftQuestions
18

Supposons que vous faites la décoration intérieure de votre salon: vous installez un lustre élégant au plafond et branchez un lampadaire élégant et assorti. Un an plus tard, votre charmante épouse décide qu'elle aimerait que la pièce soit un peu moins formelle. Quel luminaire va-t-il être plus facile à changer?

L’idée de DI est d’utiliser l’approche du «plug-in» partout. Cela peut ne pas sembler un gros problème si vous vivez dans une simple cabane sans cloison sèche et tous les fils électriques exposés. (Vous êtes un homme à tout faire - vous pouvez changer n'importe quoi!) Et de même, dans une application petite et simple, DI peut ajouter plus de complications que cela ne vaut.

Mais pour une application large et complexe, DI est indispensable pour une maintenance à long terme. Il vous permet de "débrancher" des modules entiers de votre code (le backend de la base de données, par exemple) et de les échanger avec des modules différents qui accomplissent la même chose, mais de manière totalement différente (un système de stockage en nuage, par exemple).

BTW, une discussion de DI serait incomplète sans une recommandation de Dependency Injection in .NET , par Mark Seeman. Si vous connaissez bien .NET et avez déjà l’intention de vous impliquer dans le développement de logiciels à grande échelle, c’est une lecture essentielle. Il explique le concept bien mieux que moi.

Laissez-moi vous laisser avec une dernière caractéristique essentielle de DI que votre exemple de code néglige. DI vous permet d’exploiter le vaste potentiel de flexibilité contenu dans l’adage " Programme aux interfaces ". Pour modifier légèrement votre exemple:

public void Main()
{
    ILightFixture fixture = new ClassyChandelier();
    MyRoom room = new MyRoom (fixture);
}
...
public MyRoom(ILightFixture fixture)
{
    this.MyLightFixture = fixture ; 
}

Mais MAINTENANT, car il MyRoomest conçu pour l’ interface ILightFixture , je peux facilement passer l’année prochaine et modifier une ligne de la fonction Main ("injecter une" dépendance "différente), et obtenir instantanément de nouvelles fonctionnalités dans MyRoom (sans avoir à reconstruisez ou redéployez MyRoom, s'il se trouve dans une bibliothèque séparée (cela suppose, bien entendu, que toutes vos implémentations ILightFixture soient conçues avec Liskov Substitution à l'esprit). Tous, avec juste un simple changement:

public void Main()
{
    ILightFixture fixture = new MuchLessFormalLightFixture();
    MyRoom room = new MyRoom (fixture);
}

De plus, je peux maintenant effectuer des tests unitaires MyRoom(vous êtes des tests unitaires, non?) Et utiliser un dispositif d'éclairage "simulé", ce qui me permet d'effectuer des tests MyRoomentièrement indépendants deClassyChandelier.

Bien sûr, il y a beaucoup plus d'avantages, mais ce sont ceux qui m'ont vendu l'idée.

kmote
la source
Je voulais dire merci, cela m'a été utile, mais ils ne veulent pas que vous fassiez cela, mais utilisez plutôt les commentaires pour suggérer des améliorations, alors, si vous en avez envie, vous pouvez ajouter l'un des autres avantages que vous avez mentionnés. Mais sinon, merci, cela m'a été utile.
Steve
2
Le livre que vous avez cité m'intéresse également, mais je trouve alarmant qu'il existe un livre de 584 pages sur ce sujet.
Steve
4

L'injection de dépendances consiste simplement à transmettre un paramètre, mais cela pose néanmoins quelques problèmes et le nom a pour but d'expliquer un peu pourquoi la différence entre créer un objet et en transmettre un est importante.

C'est important parce que (de toute évidence) n'importe quel objet temporel A appelle le type B, A dépend du fonctionnement de B. Ce qui signifie que si A décide quel type concret de B il utilisera, beaucoup de flexibilité dans la manière dont A peut être utilisé par des classes extérieures, sans modifier A, a été perdue.

Je mentionne cela parce que vous parlez de "rater le but" de l'injection de dépendance, et cela explique en grande partie pourquoi les gens aiment le faire. Cela peut signifier simplement passer un paramètre, mais cela peut être important.

En outre, certaines des difficultés rencontrées lors de la mise en œuvre de la DI se révèlent mieux dans les grands projets. En règle générale, vous déciderez des classes concrètes à utiliser jusqu'au sommet. Votre méthode de démarrage peut donc ressembler à:

public void Start()
{
    MySubSubInterfaceA mySubSubInterfaceA = new mySubSubInterfaceA();
    MySubSubInterfaceB mySubSubInterfaceB = new mySubSubInterfaceB();     
    MySubInterface mySubInterface = new MySubInterface(mySubSubInterfaceA,mySubSubInterfaceB);
    MyInterface myInterface = new MyInterface(MySubInterface);
    MyClass class = new MyClass(myInterface);
}

mais avec 400 autres lignes de ce genre de code fascinant. Si vous imaginez que cela continue avec le temps, l'attrait des conteneurs DI devient plus évident.

En outre, imaginez une classe qui, par exemple, implémente IDisposable. Si les classes qui l'utilisent le reçoivent par injection plutôt que de le créer elles-mêmes, comment savent-elles quand appeler Dispose ()? (Ce qui est un autre problème qu'un conteneur DI traite.)

Donc, bien sûr, l'injection de dépendance ne fait que transmettre un paramètre, mais lorsque l'on parle d'injection de dépendance, on entend réellement "transmettre un paramètre à votre objet, plutôt que de faire en sorte que votre objet instancie quelque chose lui-même", et traite de tous les avantages des inconvénients de une telle approche, éventuellement à l’aide d’un conteneur DI.

psr
la source
Cela fait beaucoup de sous-marins et d'interfaces! Voulez-vous dire de créer une interface plutôt qu'une classe qui implémente l'interface? Semble faux.
JBRWilkinson
@ JBRWilkinson Je veux dire une classe qui implémente une interface. Je devrais rendre cela plus clair. Cependant, le fait est qu'une grande application aura des classes avec des dépendances qui à leur tour auront des dépendances à tour de rôle ... La configuration manuelle de tout dans la méthode Main est le résultat de nombreuses injections de constructeur.
psr
4

Au niveau de la classe, c'est facile.

"Dependency Injection" répond simplement à la question "comment vais-je trouver mes collaborateurs" avec "ils sont poussés à vous - vous n'avez pas à aller les chercher vous-même". (Cela ressemble à - mais pas la même chose que - 'Inversion of Control' où la question "comment vais-je ordonner mes opérations sur mes entrées?" A une réponse similaire).

Le seul avantage de vos collaborateurs est que le code client peut utiliser votre classe pour composer un graphe d'objet adapté à ses besoins actuels ... vous n'avez pas prédéterminé de manière arbitraire la forme et la mutabilité du graphe en décidant en privé les types concrets et le cycle de vie de vos collaborateurs.

(Tous ces autres avantages, de testabilité, de couplage lâche, etc., découlent en grande partie de l'utilisation d'interfaces et pas tellement de la dépendance par injection, bien que la DI favorise naturellement l'utilisation d'interfaces).

Il est à noter que, si vous évitez d'instancier vos propres collaborateurs, votre classe doit donc obtenir ses collaborateurs d'un constructeur, d'une propriété ou d'un argument de méthode (cette dernière option est souvent négligée, d'ailleurs ... Il n’est pas toujours logique que les collaborateurs d’une classe fassent partie de son «état»).

Et c'est une bonne chose.

Au niveau de l'application ...

Voilà pour la vue par classe des choses. Supposons que plusieurs classes suivent la règle "ne pas instancier vos propres collaborateurs" et que vous souhaitiez en faire une application. La solution la plus simple consiste à utiliser le bon vieux code (un outil très utile pour appeler des constructeurs, des propriétés et des méthodes!) Afin de composer le graphe d'objet souhaité et de lancer des entrées. (Oui, certains objets de votre graphe seront eux-mêmes des fabriques d’objets, qui ont été transmises en tant que collaborateurs à d’autres objets de longue durée dans le graphe, prêts au service. Vous ne pouvez pas pré-construire tous les objets! ).

... votre besoin de configurer de manière "flexible" le graphe d'objet de votre application ...

En fonction de vos autres objectifs (non liés au code), vous pouvez donner à l'utilisateur final un contrôle sur le graphe d'objets ainsi déployé. Cela vous conduit vers un schéma de configuration, d’une sorte ou d’une autre, qu’il s’agisse d’un fichier texte de votre propre conception avec des paires nom / valeur, un fichier XML, un DSL personnalisé, un langage de description de graphe déclaratif tel que YAML, un langage de script impératif tel que JavaScript ou autre chose appropriée à la tâche à accomplir. Peu importe ce qu'il faut pour composer un graphe d'objet valide, de manière à répondre aux besoins de vos utilisateurs.

... peut être une force de conception significative.

Dans les circonstances les plus extrêmes de ce type, vous pouvez choisir une approche très générale et donner aux utilisateurs finaux un mécanisme général permettant de «câbler» le graphe d'objet de leur choix et même de leur permettre de fournir des réalisations concrètes d'interfaces au runtime! (Votre documentation est un joyau étincelant, vos utilisateurs sont très intelligents, ils connaissent au moins les grandes lignes du graphe d'objets de votre application, mais ils ne disposent pas d'un compilateur à portée de main). Ce scénario peut théoriquement se produire dans certaines situations «d'entreprise».

Dans ce cas, vous disposez probablement d'un langage déclaratif qui permet à vos utilisateurs d'exprimer n'importe quel type, de la composition d'un graphe d'objets de ce type et d'une palette d'interfaces que l'utilisateur final mythique peut mélanger et assortir. Pour réduire la charge cognitive de vos utilisateurs, vous préférez une approche de «configuration par convention», de sorte qu'ils n'aient plus qu'à intervenir et à passer outre le fragment objet-graphe d'intérêt, au lieu de se débattre dans son ensemble.

Vous pauvre pauvre merde!

Parce que vous n'avez pas envie d'écrire tout cela vous-même (mais sérieusement, vérifiez une liaison YAML pour votre langue), vous utilisez un cadre DI quelconque.

En fonction de la maturité de ce cadre, vous n’avez peut-être pas la possibilité d’utiliser une injection par constructeur, même lorsque cela a du sens (les collaborateurs ne changent pas pendant la durée de vie d’un objet), vous obligeant ainsi à utiliser Setter Injection (même lorsque les collaborateurs ne changent pas au cours de la vie d'un objet, et même lorsqu'il n'y a pas vraiment de raison logique pour laquelle toute implémentation concrète d'une interface doit avoir des collaborateurs d'un type spécifique). Si c'est le cas, vous êtes actuellement dans l'enfer à fort couplage, malgré les "interfaces utilisées" avec diligence dans votre code-base - horreur!

Espérons cependant que vous avez utilisé un framework DI qui vous offre une option d’injection de constructeur, et que vos utilisateurs ne sont que grincheux de ne pas perdre plus de temps à réfléchir aux choses spécifiques qu’ils ont besoin de configurer et à leur donner une interface utilisateur plus adaptée à la tâche. à portée de main. (Bien que pour être juste, vous avez probablement essayé de penser à un moyen, mais JavaEE vous a laissé tomber et vous avez dû recourir à ce hack horrible).

Note de démarrage

Vous n’utilisez jamais Google Guice, ce qui permet au codeur de se passer de la tâche de composer un graphe-objet avec du code ... en écrivant du code. Argh!

David Bullock
la source
0

Beaucoup de gens ont dit ici que DI est un mécanisme pour externaliser l’initialisation d’une variable d’instance.

Pour des exemples évidents et simples, vous n'avez pas vraiment besoin d'une "injection de dépendance". Vous pouvez le faire en transmettant un simple paramètre au constructeur, comme vous l'avez indiqué plus haut.

Cependant, je pense qu'un mécanisme "d'injection de dépendance" dédié est utile lorsque vous parlez de ressources au niveau de l'application, lorsque l'appelant et l'appelé ne savent rien de ces ressources (et ne veulent pas savoir!).

Par exemple: connexion à la base de données, contexte de sécurité, fonction de journalisation, fichier de configuration, etc.

De plus, chaque singleton est en général un bon candidat pour DI. Plus vous en avez et en utilisez, plus vous aurez besoin de DI.

Pour ce type de ressources, il est clair qu’il n’est pas logique de les déplacer dans des listes de paramètres, c’est pourquoi la DI a été inventée.

Dernière remarque, vous avez également besoin d'un conteneur DI, qui effectuera l'injection proprement dite ... (encore une fois, pour libérer l'appelant et l'appelé des détails de la mise en œuvre).

gamliela
la source
-2

Si vous transmettez un type de référence abstrait, le code appelant peut transmettre tout objet qui étend / implémente le type abstrait. Ainsi, à l'avenir, la classe qui utilise l'objet pourrait utiliser un nouvel objet créé sans jamais modifier son code.

DavidB
la source
-4

C'est une autre option en plus de la réponse d'Amon

Utiliser les constructeurs:

classe Foo {
    bar privé;
    privé Qux Qux;

    Foo privé () {
    }

    public Foo (constructeur Builder) {
        this.bar = builder.bar;
        this.qux = builder.qux;
    }

    public static class Builder {
        bar privé;
        privé Qux Qux;
        public Buider setBar (Bar Bar) {
            this.bar = bar;
            retournez ceci;
        }
        public Builder setQux (Qux qux) {
            this.qux = qux;
            retournez ceci;
        }
        public Foo build () {
            retourne un nouveau Foo (this);
        }
    }
}

// en production:
Foo Foo = nouveau Foo.Builder ()
    .setBar (nouvelle barre ())
    .setQux (new Qux ())
    .construire();

// en test:
Foo testFoo = new Foo.Builder ()
    .setBar (new BarMock ())
    .setQux (new Qux ())
    .construire();

C'est ce que j'utilise pour les dépendances. Je n'utilise aucun cadre DI. Ils se mettent en travers du chemin.

utilisateur3155341
la source
4
Cela n'ajoute pas grand chose aux autres réponses d'il y a un an et ne fournit aucune explication sur la raison pour laquelle un constructeur peut aider le demandeur à comprendre l'injection de dépendance.
@Snowman C'est une autre option à la place de DI. Je suis désolé que tu ne le vois pas comme ça. Merci pour le vote négatif.
user3155341
1
le demandeur voulait comprendre si l’ID était aussi simple que de passer un paramètre, il ne demandait pas d’alternative à l’ID.
1
Les exemples du PO sont très bons. Vos exemples apportent plus de complexité à une idée simple. Ils n'illustrent pas le problème de la question.
Adam Zuckerman
Le constructeur DI passe un paramètre. Sauf que vous passez un objet d'interface plutôt qu'un objet de classe concret. C'est cette "dépendance" à une implémentation concrète qui est résolue via DI. Le constructeur ne sait pas quel type il est en train d'être passé, il ne s'en soucie pas, il sait seulement qu'il reçoit un type qui implémente une interface. Je suggère PluralSight, il propose un excellent cours de niveau DI / CIO pour débutants.
Hardgraf