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


j'essaie de comprendre injections de dépendance (DI), et encore une fois j'ai échoué. Cela semble juste idiot. Mon code n'est jamais un désordre; J'écris à peine des fonctions et des interfaces virtuelles (bien que je le fasse une fois dans une lune bleue) et toute ma configuration est sérieusement numérisée dans une classe en utilisant json.net (parfois en utilisant un sérialiseur XML).

Je ne comprends pas très bien quel problème il résout. Cela ressemble à une façon de dire: "salut, quand vous exécutez cette fonction, renvoyez un objet de ce type et utilise ces paramètres / données."
Mais ... pourquoi devrais-je jamais utiliser ça? Note je n'ai jamais eu besoin d'utiliser object aussi, mais je comprends à quoi ça sert.

Quelles sont certaines situations réelles dans la création d'un site Web ou d'une application de bureau où l'on utiliserait DI? Je peux facilement trouver des cas pour lesquels quelqu'un voudra utiliser des interfaces / fonctions virtuelles dans un jeu, mais il est extrêmement rare (assez rare que je ne me souvienne pas d'une seule occurrence) de l'utiliser dans un code autre que le jeu.


452
2018-01-13 06:59


origine


Réponses:


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

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

(En fait, il y a des interfaces qui sont aussi 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 être les deux implémentations de IEnumerable.

Pour comprendre, vos cours dépendent souvent les uns des autres. Par exemple. vous pourriez avoir un Database classe qui accède à votre base de données (hah, surprise! ;-)), mais vous voulez aussi que cette classe fasse la journalisation de l'accès à la base de données. Supposons que vous ayez une autre classe Logger, puis Database a une dépendance à Logger.

Jusqu'ici tout va bien.

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

var logger = new Logger();

et tout va bien. C'est bien le jour où vous réalisez que vous avez besoin d'un tas d'enregistreurs: Parfois, vous voulez vous connecter à la console, parfois au système de fichiers, parfois en utilisant TCP / IP et un serveur de journalisation à distance, etc.

Et bien sûr que vous faites NE PAS vouloir changer tout votre code (en attendant vous en avez des tonnes) et remplacer toutes les lignes

var logger = new Logger();

par:

var logger = new TcpLogger();

D'abord, ce n'est pas amusant. Deuxièmement, ceci est sujet aux erreurs. Troisièmement, c'est un travail stupide et répétitif pour un singe entraîné. Donc que fais-tu?

Évidemment, c'est une très 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. La prochaine étape est que vous ne voulez pas avoir new Logger() encore et encore. Donc, vous mettez la fiabilité pour créer de nouvelles instances à une seule classe centrale, et vous obtenez du code comme:

ICanLog logger = LoggerFactory.Create();

L'usine décide elle-même du type d'enregistreur à créer. Votre code ne s'en soucie plus, et si vous voulez changer le type d'enregistreur utilisé, vous le changez une fois que: À l'intérieur de l'usine.

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

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

Quelque part cette TypeFactory a besoin de données de configuration que la classe réelle instancie lorsqu'un type d'interface spécifique est demandé, vous avez donc besoin d'un mappage. Bien sûr, vous pouvez faire ce mapping dans votre code, mais un changement de type signifie recompiler. Mais vous pouvez également placer ce mapping dans un fichier XML, par exemple .. Cela vous permet de changer la classe utilisée même après la compilation (!), Cela signifie dynamiquement, sans recompilation!

Pour vous donner un exemple utile: pensez à un logiciel qui ne se connecte pas normalement, mais lorsque votre client appelle et demande de l'aide car il a un problème, tout ce que vous lui envoyez est un fichier de configuration XML mis à jour. la journalisation est activée et votre assistance peut utiliser les fichiers journaux pour aider votre client.

Et maintenant, lorsque vous remplacez un peu les noms, vous vous retrouvez avec une simple implémentation d'un Localisateur de service, qui est l'un des deux modèles pour Inversion de contrôle (puisque vous inversez le contrôle sur qui décide quelle classe exacte à instancier).

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

Injection de dépendance est maintenant la prochaine étape dans cette ligne: Il suffit de se débarrasser de cette dépendance unique au localisateur de services: au lieu de plusieurs classes demandant au localisateur de services une implémentation pour une interface spécifique, vous annulez - encore une fois

Avec l'injection de dépendance, votre Database la classe a maintenant un constructeur qui nécessite un paramètre de type ICanLog:

public Database(ICanLog logger) { ... }

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

Et c'est là qu'intervient un cadre DI: Vous configurez vos mappages une fois de plus, puis demandez à votre infrastructure DI d'instancier votre application pour vous. Comme le Application la classe nécessite un ICanPersistData implémentation, une instance de Database est injecté - mais pour cela il faut d'abord créer une instance du type de logger configuré pour ICanLog. Etc ...

Donc, pour résumer une histoire courte: L'injection de dépendance est l'une des deux façons de supprimer les dépendances dans votre code. Il est très utile pour les changements de configuration après la compilation, et c'est une très bonne 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 service (par exemple, si vous ne savez pas à l'avance combien d'interfaces vous avez besoin d'une interface spécifique: une infrastructure DI n'injecte jamais qu'une seule instance par paramètre, mais un localisateur de service à l'intérieur d'une boucle, bien sûr), donc le plus souvent chaque cadre DI fournit également un localisateur de service.

Mais fondamentalement, c'est ça.

J'espère que cela pourra aider.

PS: Ce que j'ai décrit ici est une technique appelée injection de constructeur, il y a aussi injection de propriété où pas les paramètres du constructeur, mais les propriétés sont utilisées pour définir et résoudre les dépendances. Pensez à l'injection de propriété en tant que dépendance facultative et à l'injection de constructeur en tant que dépendances obligatoires. Mais la discussion sur ce sujet dépasse la portée de cette question.


746
2018-01-13 07:21



Je pense que beaucoup de fois les gens deviennent confus au sujet de la différence entre injection de dépendance et une injection de dépendance cadre (ou un récipient comme on l'appelle souvent).

L'injection de dépendance 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 du code comme ceci:

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 aspects importants sont la capacité de contrôler la fonctionnalité depuis un endroit central (le Main() fonction) au lieu de la diffuser tout au long de votre programme, et la possibilité de tester plus facilement chaque classe isolément (car vous pouvez transférer des objets simulés ou d’autres objets falsifiés dans son constructeur au lieu d’une valeur réelle).

L'inconvénient, bien sûr, est que vous avez maintenant une méga-fonction qui connaît toutes les classes utilisées par votre programme. C'est ce que les frameworks DI peuvent aider. Mais si vous avez du mal à comprendre pourquoi cette approche est précieuse, je vous conseille de commencer par l'injection de dépendance manuelle, afin que vous puissiez mieux apprécier les différentes fonctionnalités disponibles.


444
2018-01-13 08:22



Comme l'indiquent les autres réponses, l'injection de dépendance est un moyen de créer vos dépendances en dehors de la classe qui l'utilise. Vous les injectez de l'extérieur et prenez le contrôle de leur création loin de l'intérieur de votre classe. C'est aussi pourquoi l'injection de dépendance est une réalisation de la Inversion de contrôle (IoC) principe.

IoC est le principe, où DI est le modèle. La raison pour laquelle vous pourriez "avoir besoin de plus d'un enregistreur" n'est jamais vraiment satisfaite, pour autant que je le sache, mais la raison en est que vous en avez vraiment besoin, chaque fois que vous testez quelque chose. Un exemple:

Mon trait:

Quand je regarde une offre, je veux marquer que je l'ai regardé automatiquement, de sorte que je n'oublie pas de le faire.

Vous pourriez tester ceci comme ceci:

[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 toujours, car la date en cours sera différente de la date indiquée, même si vous mettez juste DateTime.Now dans le code de test, il peut être éteint de quelques millisecondes et échouera donc toujours. Une meilleure solution maintenant serait de créer une interface pour cela, qui vous permet de contrôler quelle heure 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'un est la chose réelle, et l'autre vous permet de faire semblant d'un moment où c'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);
}

Comme ceci, vous avez appliqué le principe de "inversion du contrôle", en injectant une dépendance (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 ici ne sont pas nécessaires car dans C # les fonctions peuvent être transmises sous forme de variables. Au lieu d’une interface, vous pouvez utiliser une Func<DateTime>pour atteindre le même. Ou, si vous adoptez une approche dynamique, vous passez simplement tout objet ayant la méthode équivalente (typage de canard), et vous n’avez pas besoin d’une interface.

Vous aurez rarement besoin de plus d'un enregistreur. Néanmoins, l'injection de dépendance est essentielle pour un code de type statique tel que Java ou C #.

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

J'espère que cela a aidé.


31
2018-01-13 12:20



Je pense que la réponse classique est de créer une application plus découplée, qui ne sait pas quelle implémentation sera utilisée pendant l'exécution.

Par exemple, nous sommes un fournisseur de paiement central, travaillant avec de nombreux fournisseurs de paiement dans le monde entier. Cependant, quand 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 boîtiers de commutation, 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;
        }
    }
}

Maintenant, imaginez que vous ayez besoin de conserver tout ce code dans une seule classe car il n'est pas découplé correctement, vous pouvez imaginer que pour chaque nouveau processeur que vous supporterez, vous devrez créer une nouvelle affaire if // switch pour chaque méthode, cela devient plus compliqué, cependant, en utilisant l'injection de dépendance (ou l'inversion de contrôle - comme on l'appelle parfois, ce qui signifie que quiconque contrôle l'exécution du programme est connu uniquement à l'exécution, et non complication) très propre 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 compile pas, je sais :)


10
2018-01-13 14:01



La principale raison d'utiliser DI est que vous voulez mettre la responsabilité de la connaissance de la mise en œuvre là où la connaissance est là. L'idée de DI est très en ligne avec l'encapsulation et le design par interface. Si le frontal demande pour certaines données, est-il sans importance pour le frontal de savoir comment le back-end résout cette question. Cela dépend du gestionnaire de requêtes.

Cela est déjà courant dans la POO depuis longtemps. Plusieurs fois en créant des 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, de sorte que le frontal sait quelle implémentation est utilisée. DI prend la conception par interface un peu plus loin, la seule chose que le frontal doit savoir, c'est la connaissance de l'interface. Entre le DYI et le DI, il y a le modèle d'un localisateur de service, car le frontal doit fournir une clé (présente dans le registre du localisateur de service) pour permettre à sa requête d'être résolue. Exemple de localisateur de service:

I_Dosomething x = ServiceLocator.returnDoing(String pKey);

DI exemple:

I_Dosomething x = DIContainer.returnThat();

L'une des exigences de DI est que le conteneur doit être capable de déterminer 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 plus d'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 d'usine.

D (b) I: Injection de dépendances et conception par interface. Cette restriction n'est cependant pas un très gros problème pratique. 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 est crucial ici.

Je préfère l'administration des contrats de service avec D (b) I dans le codage. Ils devraient aller ensemble. L'utilisation de D (b) I en tant que solution technique sans administration des contrats de service n'est pas très bénéfique à mes yeux, car DI n'est alors qu'une couche d'encapsulation supplémentaire. Mais lorsque vous pouvez l'utiliser avec l'administration organisationnelle, vous pouvez vraiment utiliser le principe d'organisation D (b) I. Il peut vous aider à long terme à structurer la communication avec le client et les autres départements techniques sur des sujets tels que le test, le versioning 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 dans le temps, puis lorsque vous la faites explicite en utilisant D (b) I. Tout se résume à la maintenance, qui est au fil du temps et non à la fois. :-)


5
2018-01-13 09:46