636 votes

Pourquoi utilise-t-on l'injection de dépendances ?

J'essaie de comprendre injections de dépendances (DI), et une fois de plus j'ai échoué. Cela semble tout simplement stupide. Mon code n'est jamais un fouillis ; je n'écris pratiquement pas de fonctions et d'interfaces virtuelles (bien que je le fasse une fois de temps en temps) et toute ma configuration est sérialisée comme par magie dans une classe à l'aide de json.net (parfois à l'aide d'un sérialiseur XML).

Je ne comprends pas bien quel problème il résout. Ça ressemble à un moyen de dire : "Bonjour, lorsque vous utilisez cette fonction, renvoyez un objet de ce type et utilisant ces paramètres/données".
Mais... pourquoi l'utiliserais-je ? Notez que je n'ai jamais eu besoin d'utiliser object aussi, mais je comprends à quoi ça sert.

Quelles sont les situations réelles dans la construction d'un site Web ou d'une application de bureau où l'on utilise l'AI ? Je peux facilement trouver des cas pour lesquels quelqu'un pourrait vouloir utiliser des interfaces/fonctions virtuelles dans un jeu, mais il est extrêmement rare (suffisamment rare pour que je ne me souvienne pas d'un seul cas) d'utiliser cela dans du code non lié à un jeu.

3 votes

Cela peut aussi être une information utile : martinfowler.com/articles/injection.html

1 votes

0 votes

@Steven Oui. En fait, pas dans mon code C#, mais je suis prêt à donner une explication. Et non, je n'ai jamais eu de douleurs ou de problèmes avec ça. Mon code (et celui de mes équipes) est toujours très bien conçu.

945voto

Golo Roden Points 10272

Tout d'abord, je veux expliquer une hypothèse que je fais pour cette réponse. Elle n'est pas toujours vraie, mais assez souvent :

Les interfaces sont des adjectifs ; les classes sont des noms.

(En fait, il existe aussi des interfaces qui sont des noms, mais je veux généraliser ici).

Ainsi, par exemple, une interface peut être quelque chose comme IDisposable , IEnumerable ou IPrintable . Une classe est une implémentation réelle d'une ou plusieurs de ces interfaces : List ou Map peuvent tous deux être des implémentations de IEnumerable .

Pour aller droit au but : Souvent, vos classes dépendent les unes des autres. Par exemple, vous pouvez avoir un Database qui accède à votre base de données (hah, surprise ! ;-)), mais vous voulez aussi que cette classe fasse de la journalisation sur l'accès à la base de données. Supposons que vous ayez une autre classe Logger alors Database a un lien de dépendance avec Logger .

Jusqu'à présent, tout va bien.

Vous pouvez modéliser cette dépendance dans votre Database avec la ligne suivante :

var logger = new Logger();

et tout va bien. Tout va bien jusqu'au jour où vous réalisez que vous avez besoin d'un tas de loggers : Parfois, vous voulez vous connecter à la console, parfois au système de fichiers, parfois à l'aide de TCP/IP et d'un serveur d'enregistrement distant, et ainsi de suite...

Et bien sûr, vous le faites PAS vous voulez changer tout votre code (entre-temps vous en avez des milliards) et remplacer toutes les lignes

var logger = new Logger();

par :

var logger = new TcpLogger();

Premièrement, ce n'est pas amusant. Deuxièmement, c'est source d'erreurs. Enfin, c'est un travail stupide et répétitif pour un singe savant. Alors, que faites-vous ?

Il est évident que c'est une assez bonne idée d'introduire une interface ICanLog (ou similaire) qui est mis en œuvre par tous les différents enregistreurs. Donc l'étape 1 de votre code est que vous faites :

ICanLog logger = new Logger();

Maintenant, l'inférence de type ne change plus de type, vous avez toujours une seule interface à développer. L'étape suivante est que vous ne voulez pas avoir de new Logger() encore et encore. Vous confiez donc la fiabilité de la création de nouvelles instances à une classe d'usine unique et centrale, et vous obtenez un code tel que :

ICanLog logger = LoggerFactory.Create();

La fabrique elle-même décide du type de logger à créer. Votre code ne s'en soucie plus, et si vous voulez changer le type de logger utilisé, vous le modifiez une fois : A l'intérieur de l'usine.

Maintenant, bien sûr, vous pouvez généraliser cette usine, et la faire fonctionner pour n'importe quel type :

ICanLog logger = TypeFactory.Create<ICanLog>();

Quelque part, cette TypeFactory a besoin de données de configuration pour savoir quelle classe réelle doit être instanciée lorsqu'un type d'interface spécifique est demandé, ce qui nécessite un mappage. Bien sûr, vous pouvez faire ce mappage dans votre code, mais alors un changement de type signifie une recompilation. Mais vous pouvez également placer ce mappage dans un fichier XML, par exemple. Cela vous permet de changer la classe réellement utilisée même après la compilation ( !), c'est-à-dire dynamiquement, sans recompilation !

Pour vous donner un exemple utile à ce sujet : Pensez à un logiciel qui n'enregistre pas normalement, mais lorsque votre client appelle et demande de l'aide parce qu'il a un problème, tout ce que vous lui envoyez est un fichier de configuration XML mis à jour, et maintenant il a l'enregistrement activé, et votre support peut utiliser les fichiers journaux pour aider votre client.

Et maintenant, en remplaçant un peu les noms, on se retrouve avec une implémentation simple d'une Localisateur de services qui est l'un des deux modèles de Inversion du contrôle (puisque vous inversez le contrôle sur qui décide de la classe exacte à instancier).

Dans l'ensemble, cela réduit les dépendances dans votre code, mais maintenant tout votre code a une dépendance envers le localisateur de service central et unique.

Injection de dépendances est maintenant la prochaine étape de cette ligne : Il suffit de se débarrasser de cette dépendance unique au localisateur de services : Au lieu que différentes classes demandent au localisateur de services une implémentation pour une interface spécifique, vous reprenez - une fois de plus - le contrôle de qui instancie quoi.

Avec l'injection de dépendances, votre Database a maintenant un constructeur qui requiert un paramètre de type ICanLog :

public Database(ICanLog logger) { ... }

Maintenant votre base de données a toujours un logger à utiliser, mais elle ne sait plus d'où vient ce logger.

Et c'est là qu'un cadre DI entre en jeu : Vous configurez à nouveau vos mappings, puis vous demandez à votre framework DI d'instancier votre application pour vous. Comme le Application nécessite un ICanPersistData une instance de Database est injecté - mais pour cela il doit d'abord créer une instance du type de logger qui est configuré pour ICanLog . Et ainsi de suite...

Donc, pour faire court : l'injection de dépendances est l'une des deux façons de supprimer les dépendances dans votre code. C'est très utile pour les changements de configuration après la compilation, et c'est une excellente chose pour les tests unitaires (car il est très facile d'injecter des stubs et / ou des mocks).

En pratique, il y a des choses que vous ne pouvez pas faire sans un localisateur de services (par exemple, si vous ne savez pas à l'avance combien d'instances vous avez besoin d'une interface spécifique : Un framework DI n'injecte toujours qu'une seule instance par paramètre, mais vous pouvez appeler un localisateur de services à l'intérieur d'une boucle, bien sûr), c'est pourquoi le plus souvent chaque framework DI fournit également un localisateur de services.

Mais en fait, c'est tout.

P.S. : Ce que j'ai décrit ici est une technique appelée injection de constructeur il y a aussi injection de biens où ce ne sont pas des paramètres de constructeur, mais des propriétés qui sont utilisées pour définir et résoudre les dépendances. Considérez l'injection de propriétés comme une dépendance facultative, et l'injection de constructeurs comme une dépendance obligatoire. Mais la discussion sur ce point dépasse le cadre de cette question.

0 votes

Ok, vous avez écrit "L'injection de dépendances est l'une des deux façons". Quelle est l'autre façon ? Parce que... Je sais déjà ce que je ferais plutôt que ça et je ne sais pas si nous pensons la même chose. -edit- fyi j'ai presque upvoted. idk pourquoi quelqu'un DV vous

2 votes

La façon dont j'aurais procédé est de TOUT LAISSER COMME C'ÉTAIT (c'est-à-dire ne rien changer). new Logger() ni d'écrire LoggerFactory.Create(); n'importe où. Je le changerais pour que Logger ait un membre ICanLog qui est défini dans le constructeur en lisant le fichier XML et en lui transmettant toutes les commandes. Aucune modification du code n'est nécessaire, si ce n'est de renommer la classe Logger originale en SomeConcreteLogger, puis de créer une classe de transfert en tant que Logger .

7 votes

Bien sûr, vous pouvez aussi le faire de cette façon, mais vous devrez alors implémenter cette logique dans chaque classe qui fournira un support pour l'échange d'implémentation. Cela signifie beaucoup de code dupliqué et redondant, et cela signifie également que vous devez toucher une classe existante et la réécrire partiellement, une fois que vous décidez que vous en avez besoin maintenant. DI vous permet d'utiliser ceci sur n'importe quelle classe arbitraire, vous ne devez pas les écrire d'une manière spéciale (sauf définir les dépendances comme paramètres dans le constructeur).

593voto

Daniel Pryden Points 22167

Je pense que souvent les gens se trompent sur la différence entre injection de dépendances et une injection de dépendances cadre (ou un conteneur comme on l'appelle souvent).

L'injection de dépendances est un concept très simple. Au lieu de ce code :

public class A {
  private B b;

  public A() {
    this.b = new B(); // A *depends on* B
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  A a = new A();
  a.DoSomeStuff();
}

vous écrivez un code comme celui-ci :

public class A {
  private B b;

  public A(B b) { // A now takes its dependencies as arguments
    this.b = b; // look ma, no "new"!
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  B b = new B(); // B is constructed here instead
  A a = new A(b);
  a.DoSomeStuff();
}

Et c'est tout. Sérieusement. Cela vous donne une tonne d'avantages. Deux d'entre eux sont importants : la possibilité de contrôler les fonctionnalités à partir d'un endroit central (le site Web de l'entreprise) et la possibilité d'utiliser des outils de gestion de la qualité. Main() ) au lieu de l'étendre à l'ensemble de votre programme, et la possibilité de tester plus facilement chaque classe de manière isolée (parce que vous pouvez passer des objets fantaisie ou d'autres objets factices dans son constructeur au lieu d'une valeur réelle).

L'inconvénient, bien sûr, est que vous avez maintenant une mégafonction qui connaît toutes les classes utilisées par votre programme. C'est ce que les frameworks DI peuvent faire. Mais si vous avez du mal à comprendre l'intérêt de cette approche, je vous recommande de commencer par l'injection manuelle de dépendances, afin de mieux apprécier ce que les différents frameworks peuvent vous apporter.

11 votes

Pourquoi préférer le second code plutôt que le premier ? Le premier ne contient que le nouveau mot-clé, en quoi cela peut-il m'aider ?

23 votes

@user962206 pensez à la façon dont vous testeriez A indépendamment de B.

87 votes

@user962206, aussi, pensez à ce qui se passerait si B avait besoin de certains paramètres dans son constructeur : afin de l'instancier, A devrait connaître ces paramètres, quelque chose qui pourrait être complètement étranger à A (il veut juste dépendre de B, pas de ce dont B dépend). Passer un B déjà construit (ou n'importe quelle sous-classe ou mock de B d'ailleurs) au constructeur de A résout ce problème et fait que A dépend uniquement de B :)

44voto

cessor Points 114

Comme les autres réponses l'ont indiqué, l'injection de dépendances est un moyen de créer vos dépendances en dehors de la classe qui les utilise. Vous les injectez de l'extérieur, et prenez le contrôle de leur création en dehors de votre classe. C'est également la raison pour laquelle l'injection de dépendances est une réalisation de l'approche de la Inversion du contrôle (IoC).

IoC est le principe, tandis que DI est le modèle. La raison pour laquelle vous pourriez "avoir besoin de plus d'un logger" n'est jamais rencontrée, d'après mon expérience, mais la raison réelle est que vous en avez vraiment besoin, chaque fois que vous testez quelque chose. Un exemple :

Mon reportage :

Lorsque je regarde une offre, je veux marquer que je l'ai regardée automatiquement, afin de ne pas oublier de le faire.

Vous pouvez tester cela comme suit :

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var formdata = { . . . }

    // System under Test
    var weasel = new OfferWeasel();

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(new DateTime(2013,01,13,13,01,0,0));
}

Donc quelque part dans le OfferWeasel il vous construit une offre Objet comme ceci :

public class OfferWeasel
{
    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = DateTime.Now;
        return offer;
    }
}

Le problème ici est que ce test échouera probablement toujours, puisque la date qui est définie sera différente de la date qui est affirmée, même si vous mettez simplement DateTime.Now dans le code de test, il peut être décalé de quelques millisecondes et échouera donc toujours. Une meilleure solution serait de créer une interface pour cela, qui vous permette de contrôler l'heure qui sera définie :

public interface IGotTheTime
{
    DateTime Now {get;}
}

public class CannedTime : IGotTheTime
{
    public DateTime Now {get; set;}
}

public class ActualTime : IGotTheTime
{
    public DateTime Now {get { return DateTime.Now; }}
}

public class OfferWeasel
{
    private readonly IGotTheTime _time;

    public OfferWeasel(IGotTheTime time)
    {
        _time = time;
    }

    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = _time.Now;
        return offer;
    }
}

L'interface est l'abstraction. L'une est la chose réelle, et l'autre vous permet de simuler un moment où cela est nécessaire. Le test peut alors être modifié comme ceci :

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var date = new DateTime(2013, 01, 13, 13, 01, 0, 0);
    var formdata = { . . . }

    var time = new CannedTime { Now = date };

    // System under test
    var weasel= new OfferWeasel(time);

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(date);
}

Ainsi, vous avez appliqué le principe de "l'inversion de contrôle", en injectant une dépendance (pour obtenir l'heure actuelle). La principale raison de faire cela est de faciliter les tests unitaires isolés, il existe d'autres façons de le faire. Par exemple, une interface et une classe ne sont pas nécessaires, car en C#, les fonctions peuvent être transmises comme des variables. Func<DateTime> pour obtenir le même résultat. Ou, si vous adoptez une approche dynamique, vous passez simplement tout objet qui possède la méthode équivalente ( dactylographie du canard ), et vous n'avez pas du tout besoin d'une interface.

Vous n'aurez pratiquement jamais besoin de plus d'un enregistreur. Néanmoins, l'injection de dépendances est essentielle pour le code statiquement typé tel que Java ou C#.

Et... Il convient également de noter qu'un objet ne peut remplir correctement son rôle qu'au moment de l'exécution, si toutes ses dépendances sont disponibles, et qu'il n'est donc pas très utile de mettre en place l'injection de propriétés. À mon avis, toutes les dépendances devraient être satisfaites lorsque le constructeur est appelé, donc l'injection de constructeur est la chose à faire.

J'espère que ça vous a aidé.

6 votes

En fait, ça ressemble à une terrible solution. J'écrirais certainement du code plus comme Réponse de Daniel Pryden suggérer mais pour ce test unitaire spécifique, je ferais simplement DateTime.Now avant et après la fonction et je vérifierais si l'heure est entre les deux ? Ajouter plus d'interfaces/beaucoup plus de lignes de code me semble être une mauvaise idée.

4 votes

Je n'aime pas les exemples génériques A(B) et je n'ai jamais pensé qu'un logger devait avoir 100 implémentations. C'est un exemple que j'ai récemment rencontré et c'est l'une des 5 façons de le résoudre, où l'une d'entre elles inclut l'utilisation de PostSharp. Il illustre une approche classique d'injection de ctor basée sur une classe. Pourriez-vous fournir un meilleur exemple du monde réel où vous avez rencontré une bonne utilisation de DI ?

2 votes

Je n'ai jamais vu une bonne utilisation de DI. C'est pourquoi j'ai écrit la question.

17voto

Itai Sagi Points 2026

Je pense que la réponse classique consiste à créer une application plus découplée, qui n'a aucune connaissance de l'implémentation qui sera utilisée pendant l'exécution.

Par exemple, nous sommes un fournisseur de paiement central, qui travaille avec de nombreux fournisseurs de paiement dans le monde entier. Cependant, lorsqu'une demande est faite, je n'ai aucune idée du processeur de paiement que je vais appeler. Je pourrais programmer une classe avec une tonne de switch cases, tels que :

class PaymentProcessor{

    private String type;

    public PaymentProcessor(String type){
        this.type = type;
    }

    public void authorize(){
        if (type.equals(Consts.PAYPAL)){
            // Do this;
        }
        else if(type.equals(Consts.OTHER_PROCESSOR)){
            // Do that;
        }
    }
}

Imaginez maintenant que vous devrez maintenir tout ce code dans une seule classe parce qu'il n'est pas découplé correctement, vous pouvez imaginer que pour chaque nouveau processeur que vous prendrez en charge, vous devrez créer un nouveau cas if // switch pour chaque méthode, cela ne fait que se compliquer, cependant, en utilisant l'injection de dépendance (ou l'inversion de contrôle - comme on l'appelle parfois, ce qui signifie que celui qui contrôle l'exécution du programme n'est connu qu'au moment de l'exécution, et non de la complication), vous pourriez réaliser quelque chose de très soigné et maintenable.

class PaypalProcessor implements PaymentProcessor{

    public void authorize(){
        // Do PayPal authorization
    }
}

class OtherProcessor implements PaymentProcessor{

    public void authorize(){
        // Do other processor authorization
    }
}

class PaymentFactory{

    public static PaymentProcessor create(String type){

        switch(type){
            case Consts.PAYPAL;
                return new PaypalProcessor();

            case Consts.OTHER_PROCESSOR;
                return new OtherProcessor();
        }
    }
}

interface PaymentProcessor{
    void authorize();
}

** Le code ne sera pas compilé, je sais :)

0 votes

+1 parce que vous semblez dire que vous en avez besoin lorsque vous utilisez des méthodes/interfaces virtuelles. Mais c'est encore rare. Je le passerais quand même en tant que new ThatProcessor() plutôt que d'utiliser un cadre

0 votes

@ItaiS Vous pouvez éviter les innombrables commutations avec le design pattern class factory. Utiliser la réflexion System.Reflection.Assembly.GetExecutingAssembly().CreateInstance()

0 votes

@domenicr bien sûr ! mais je voulais l'expliquer par un exemple simplifié.

7voto

Loek Bergman Points 1072

La principale raison d'utiliser le DI est que vous voulez placer la responsabilité de la connaissance de l'implémentation là où se trouve la connaissance. L'idée du DI est très proche de l'encapsulation et de la conception par interface. Si le front-end demande des données au back-end, il est sans importance pour le front-end de savoir comment le back-end résout cette question. C'est au requesthandler de le faire.

C'est déjà courant dans la POO depuis longtemps. Souvent, la création de morceaux de code comme :

I_Dosomething x = new Impl_Dosomething();

L'inconvénient est que la classe d'implémentation est toujours codée en dur, ce qui permet au frontal de savoir quelle implémentation est utilisée. DI pousse la conception par interface un peu plus loin, la seule chose que le front-end doit savoir est la connaissance de l'interface. Entre DYI et DI se trouve le modèle d'un localisateur de services, car le front-end doit fournir une clé (présente dans le registre du localisateur de services) pour que sa demande soit résolue. Exemple de localisateur de services :

I_Dosomething x = ServiceLocator.returnDoing(String pKey);

Exemple de DI :

I_Dosomething x = DIContainer.returnThat();

L'une des exigences de l'ID est que le conteneur doit être capable de savoir quelle classe est l'implémentation de quelle interface. Par conséquent, un conteneur DI nécessite une conception fortement typée et une seule implémentation pour chaque interface en même temps. Si vous avez besoin de plusieurs implémentations d'une interface en même temps (comme une calculatrice), vous avez besoin du localisateur de services ou du modèle de conception "factory".

D(b)I : injection de dépendances et conception par interface. Cette restriction n'est cependant pas un problème pratique très important. L'avantage d'utiliser D(b)I est qu'il sert la communication entre le client et le fournisseur. Une interface est une perspective sur un objet ou un ensemble de comportements. Ce dernier point est crucial ici.

Je préfère l'administration des contrats de service avec D(b)I dans le codage. Ils doivent aller ensemble. L'utilisation de D(b)I comme solution technique sans administration organisationnelle des contrats de service n'est pas très bénéfique de mon point de vue, car DI n'est alors qu'une couche supplémentaire d'encapsulation. Mais lorsque vous pouvez l'utiliser avec l'administration organisationnelle, vous pouvez vraiment tirer parti du principe d'organisation qu'offre D(b)I. Il peut vous aider à long terme. Il peut vous aider à long terme à structurer la communication avec le client et les autres départements techniques sur des sujets tels que les tests, les versions et le développement d'alternatives. Lorsque vous avez une interface implicite comme dans une classe codée en dur, elle est beaucoup moins communicable au fil du temps que lorsque vous la rendez explicite en utilisant le D(b)I. Tout se résume à la maintenance, qui s'effectue dans le temps et non à un moment donné :-)

1 votes

"L'inconvénient est que la classe d'implémentation est toujours codée en dur" <-- la plupart du temps, il n'y a qu'une seule implémentation et comme je l'ai dit, je ne peux pas penser à un code autre que le jeu qui nécessite une interface qui n'est pas déjà intégrée (.NET).

0 votes

@acidzombie24 Peut-être ... mais comparez l'effort pour mettre en œuvre une solution utilisant DI dès le début à l'effort pour changer une solution non DI plus tard si vous avez besoin d'interfaces. Je choisirais presque toujours la première option. Il vaut mieux payer maintenant 100$ que d'avoir à payer 100.000$ demain.

1 votes

@GoloRoden En effet, la maintenance est la question clé avec des techniques comme le D(b)I. C'est 80% des coûts d'une application. Une conception dans laquelle le comportement requis est explicité à l'aide d'interfaces dès le départ fait gagner beaucoup de temps et d'argent à l'organisation par la suite.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X