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


J'essaie de comprendre les injections de dépendance (DI), et encore une fois j'ai échoué. Cela semble juste idiot. Mon code n'est jamais un gâchis; 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 magiquement sérialisée dans une classe en utilisant json.net (utilisant parfois 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 utiliserais-je ça? Remarque: Je n'ai jamais eu besoin d'utiliser d' object , mais je comprends à quoi cela sert.

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


Answers



Premièrement, 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 de plusieurs de ces interfaces: List ou Map peuvent toutes deux être des implémentations de IEnumerable .

Pour comprendre: Souvent vos classes dépendent les unes des autres. Par exemple, vous pourriez avoir une classe Database qui accède à votre base de données (hah, surprise! ;-)), mais vous voulez aussi que cette classe fasse de la journalisation pour accéder à la base de données. Supposons que vous ayez une autre classe Logger , alors Database a une dépendance à Logger .

Jusqu'ici tout va bien.

Vous pouvez modéliser cette dépendance dans votre classe Database 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, vous ne voulez pas changer tout votre code (en attendant vous avez des gazillions) 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) implémentée par tous les différents loggers. 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 de 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 elle-même décide quel 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 : à 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 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 mettre cette cartographie dans un fichier XML, par exemple. Cela vous permet de changer la classe réellement utilisée même après la compilation (!), Ce qui 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 parce qu'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, quand vous remplacez un peu les noms, vous finissez avec une implémentation simple d'un localisateur de service , qui est l'un des deux modèles d' inversion de 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 au localisateur central de service unique.

L'injection de dépendance est maintenant l'étape suivante de cette ligne: Débarrassez-vous de cette dépendance unique au localisateur de service: au lieu de diverses classes demandant au localisateur de service une implémentation pour une interface spécifique, vous retournez le contrôle sur qui instancie .

Avec l'injection de dépendance, votre classe Database 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 il 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 la classe Application requiert une implémentation ICanPersistData , une instance de Database est injectée - mais pour cela, elle doit d'abord créer une instance du type d'enregistreur 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 de fois vous avez besoin d'une interface spécifique: Un cadre DI injecte toujours une seule instance par paramètre, mais vous pouvez appeler 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 tout.

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 de paramètres constructeur, mais des 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.




Je pense que les gens sont souvent déconcertés par la différence entre l' injection de dépendance et un framework d' injection de dépendance (ou un conteneur 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 fonctions importantes sont la possibilité de contrôler la fonctionnalité à partir d'un emplacement central (la fonction Main() ) au lieu de la répartir dans votre programme et de tester plus facilement chaque classe isolément (car vous pouvez passer des simulacres ou d'autres objets truqué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 utile, je vous recommande de commencer par l'injection manuelle de dépendance, afin que vous puissiez mieux apprécier ce que les différents frameworks peuvent faire pour vous.




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 du principe Inversion de contrôle (IoC).

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 réellement rencontrée, d'après mon expérience, mais la raison en est que vous en avez vraiment besoin chaque fois que vous testez quelque chose. Un exemple:

Ma caractéristique

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 probablement toujours, puisque la date qui est définie différera de la date à laquelle il est affirmé, même si vous mettez simplement DateTime.Now dans le code de test, il peut être désactivé 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 changé 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 ne sont pas nécessaires car les fonctions C # peuvent être transmises en tant que variables. Ainsi, au lieu d'une interface, vous pouvez utiliser un Func<DateTime> pour obtenir la même chose. Ou, si vous adoptez une approche dynamique, vous devez simplement passer un objet qui a la méthode équivalente ( typage de canard ), et vous n'avez pas du tout besoin d'une interface.

Vous aurez rarement besoin de plus d'un enregistreur. Néanmoins, l'injection de dépendances est essentielle pour le code typé statiquement 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, donc il n'y a pas beaucoup d'utilisation dans la configuration de l'injection de propriété. À mon avis, toutes les dépendances devraient être satisfaites lorsque le constructeur est appelé, donc l'injection de constructeur est la chose à suivre.

J'espère que cela a aidé.




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 services 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 cas 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;
        }
    }
}

Imaginez maintenant que vous deviez 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 allez prendre en charge, vous devrez créer un nouveau cas if // switch pour chaque méthode, cela devient plus compliqué, cependant, en utilisant Dependency Injection (ou Inversion of Control - comme on l'appelle parfois, ce qui signifie que celui qui contrôle l'exécution du programme n'est connu qu'à l'exécution, et pas de complication), vous pourriez réaliser quelque chose 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 compilera pas, je sais :)




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

C'est déjà commun dans OOP 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 encore codée en dur, donc le frontal a la connaissance de l'implémentation utilisée. DI va encore plus loin dans la conception par interface, la seule chose que le frontal a besoin de savoir est la connaissance de l'interface. Entre le DYI et DI est le modèle d'un localisateur de service, parce que le frontal doit fournir une clé (présente dans le registre du localisateur de service) pour que sa demande soit résolue. Exemple de localisateur de service:

I_Dosomething x = ServiceLocator.returnDoing(String pKey);

Exemple de DI:

I_Dosomething x = DIContainer.returnThat();

L'une des exigences de DI est que le conteneur doit être capable de trouver quelle classe est la mise en œuvre 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 service 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 comme solution technique sans administration organisationnelle des contrats de service n'est pas très bénéfique à mon point de vue, car DI n'est alors qu'une couche supplémentaire d'encapsulation. Mais quand 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. :-)