Je reçois une injection de dépendance, mais quelqu'un peut-il m'aider à comprendre le besoin d'un conteneur IoC?

15

Je m'excuse si cela semble être une autre répétition de la question, mais chaque fois que je trouve un article sur le sujet, il parle principalement de ce qu'est DI. Donc, je reçois DI, mais j'essaie de comprendre le besoin d'un conteneur IoC, dans lequel tout le monde semble se lancer. Le but d'un conteneur IoC est-il vraiment juste de "résoudre automatiquement" l'implémentation concrète des dépendances? Peut-être que mes classes ont tendance à ne pas avoir plusieurs dépendances et c'est peut-être pourquoi je ne vois pas le problème, mais je veux m'assurer que je comprends correctement l'utilité du conteneur.

Je divise généralement ma logique métier en une classe qui pourrait ressembler à ceci:

public class SomeBusinessOperation
{
    private readonly IDataRepository _repository;

    public SomeBusinessOperation(IDataRespository repository = null)
    {
        _repository = repository ?? new ConcreteRepository();
    }

    public SomeType Run(SomeRequestType request)
    {
        // do work...
        var results = _repository.GetThings(request);

        return results;
    }
}

Il n'a donc qu'une seule dépendance, et dans certains cas, il peut en avoir une deuxième ou une troisième, mais pas souvent. Donc, tout ce qui appelle cela peut passer son propre référentiel ou lui permettre d'utiliser le référentiel par défaut.

En ce qui concerne ma compréhension actuelle d'un conteneur IoC, tout ce que fait le conteneur est de résoudre IDataRepository. Mais si c'est tout ce qu'il fait, alors je n'y vois pas une tonne de valeur car mes classes opérationnelles définissent déjà un repli lorsqu'aucune dépendance n'est passée. Donc, le seul autre avantage auquel je peux penser est que si j'ai plusieurs opérations comme ceci utilise le même dépôt de secours, je peux changer ce dépôt en un seul endroit qui est le registre / usine / conteneur. Et c'est très bien, mais c'est tout?

Sinaesthetic
la source
1
Souvent, avoir une version de secours par défaut de la dépendance n'a pas vraiment de sens.
Ben Aaronson
Qu'est-ce que vous voulez dire? Le "repli" est la classe concrète qui est utilisée presque tout le temps sauf dans les tests unitaires. En fait, ce serait la même classe enregistrée dans le conteneur.
Sinaesthetic
Oui, mais avec le conteneur: (1) tous les autres objets du conteneur obtiennent la même instance de ConcreteRepositoryet (2) vous pouvez fournir des dépendances supplémentaires à ConcreteRepository(une connexion à la base de données serait courante, par exemple).
Jules
@Sinaesthetic Je ne veux pas dire que c'est toujours une mauvaise idée, mais souvent ce n'est pas approprié. Par exemple, cela vous empêcherait de suivre l'architecture oignon avec vos références de projet. Il peut également n'y avoir aucune implémentation par défaut claire. Et comme Jules le dit, les conteneurs IOC gèrent non seulement la sélection du type de dépendance, mais font des choses comme le partage d'instances et la gestion du cycle de vie
Ben Aaronson
Je vais me faire un t-shirt qui lit "Paramètres de fonction - l'injection de dépendance ORIGINAL!"
Graham

Réponses:

2

Le conteneur IoC ne concerne pas le cas où vous avez une dépendance. Il s'agit du cas où vous avez 3 dépendances et elles ont plusieurs dépendances qui ont des dépendances, etc.

Il vous aide également à centraliser la résolution d'une dépendance et la gestion du cycle de vie des dépendances.

Signe
la source
10

Il existe un certain nombre de raisons pour lesquelles vous souhaiterez peut-être utiliser un conteneur IoC.

DLL non référencées

Vous pouvez utiliser un conteneur IoC pour résoudre une classe concrète à partir d'une DLL non référencée. Cela signifie que vous pouvez prendre des dépendances entièrement sur l'abstraction - c'est-à-dire l'interface.

Évitez l'utilisation de new

Un conteneur IoC signifie que vous pouvez supprimer complètement l'utilisation du newmot - clé pour créer une classe. Cela a deux effets. Le premier est qu'il dissocie vos classes. La seconde (qui est liée) est que vous pouvez déposer des simulations pour les tests unitaires. C'est incroyablement utile, en particulier lorsque vous interagissez avec un long processus.

Écrivez contre les abstractions

L'utilisation d'un conteneur IoC pour résoudre vos dépendances concrètes vous permet d'écrire votre code contre des abstractions, plutôt que d'implémenter chaque classe concrète dont vous avez besoin selon vos besoins. Par exemple, vous pourriez avoir besoin de votre code pour lire les données d'une base de données. Au lieu d'écrire la classe d'interaction de la base de données, vous écrivez simplement une interface pour elle et vous codez par rapport à cela. Vous pouvez utiliser une maquette pour tester la fonctionnalité du code que vous développez au fur et à mesure que vous le développez, plutôt que de compter sur le développement de la classe d'interaction de base de données concrète avant de pouvoir tester l'autre code.

Évitez le code fragile

Une autre raison d'utiliser un conteneur IoC est qu'en s'appuyant sur le conteneur IoC pour résoudre vos dépendances, vous évitez de devoir changer chaque appel à un constructeur de classe lorsque vous ajoutez ou supprimez une dépendance. Le conteneur IoC résoudra automatiquement vos dépendances. Ce n'est pas un problème énorme lorsque vous créez une classe une fois, mais c'est un problème gigantesque lorsque vous créez la classe dans une centaine d'endroits.

Gestion à vie et nettoyage non géré des ressources

La dernière raison que je mentionnerai est la gestion des durées de vie des objets. Les conteneurs IoC offrent souvent la possibilité de spécifier la durée de vie d'un objet. Il est très logique de spécifier la durée de vie d'un objet dans un conteneur IoC plutôt que d'essayer de le gérer manuellement dans le code. La gestion manuelle de la durée de vie peut être très difficile. Cela peut être utile lorsque vous traitez des objets qui doivent être éliminés. Au lieu de gérer manuellement l'élimination de vos objets, certains conteneurs IoC géreront l'élimination pour vous, ce qui peut aider à prévenir les fuites de mémoire et à simplifier votre base de code.

Le problème avec l'exemple de code que vous avez fourni est que la classe que vous écrivez a une dépendance concrète sur la classe ConcreteRepository. Un conteneur IoC supprimerait cette dépendance.

Stephen
la source
22
Ce ne sont pas des avantages des conteneurs IoC, ce sont des avantages de l'injection de dépendance, ce qui peut être fait facilement avec l'
Ben Aaronson
L'écriture d'un bon code DI sans conteneur IoC peut en fait être assez difficile. Oui, il y a un certain chevauchement dans les avantages, mais ce sont tous des avantages qui sont mieux exploités avec un conteneur IoC.
Stephen
Eh bien, les deux dernières raisons que vous avez ajoutées depuis mon commentaire sont plus spécifiques au conteneur et sont à mon avis des arguments très forts
Ben Aaronson
«Évitez d'utiliser de nouveaux» - entrave également l'analyse de code statique, vous devez donc commencer à utiliser quelque chose comme ceci: hmemcpy.github.io/AgentMulder . Les autres avantages que vous décrivez dans ce paragraphe concernent la DI et non l'IoC. De plus, vos classes seront toujours couplées si vous évitez d'utiliser new mais utiliserez des types concrets au lieu d'interfaces pour les paramètres.
Den
1
Globalement, l'IoC est présenté comme quelque chose sans défauts, par exemple, il n'est pas fait mention du gros inconvénient: pousser une classe d'erreurs dans le runtime au lieu de la compilation.
Den
2

Selon le principe de la responsabilité unique, chaque classe ne doit avoir qu'une seule responsabilité. La création de nouvelles instances de classes n'est qu'une autre responsabilité, vous devez donc encapsuler ce type de code dans une ou plusieurs classes. Vous pouvez le faire en utilisant n'importe quel modèle de création, par exemple des usines, des constructeurs, des conteneurs DI, etc.

Il existe d'autres principes comme l'inversion de contrôle et l'inversion de dépendance. Dans ce contexte, elles sont liées à l'instanciation des dépendances. Ils indiquent que les classes de haut niveau doivent être découplées des classes de bas niveau (dépendances) qu'elles utilisent. Nous pouvons découpler les choses en créant des interfaces. Les classes de bas niveau doivent donc implémenter des interfaces spécifiques et les classes de haut niveau doivent utiliser des instances de classes qui implémentent ces interfaces. (Remarque: la contrainte d'interface uniforme REST applique la même approche au niveau du système.)

La combinaison de ces principes par l'exemple (désolé pour le code de faible qualité, j'ai utilisé un langage ad hoc au lieu de C #, car je ne le sais pas):

  1. Pas de SRP, pas d'IoC

    class SomeHighLevelService
    {
        public doFooBar(){
            Crap crap = doFoo();
            doBar(crap);
        }
    
        public Crap doFoo(){
            //...
            return crap;
        }
    
        public doBar(Crap crap){
            //...
        }
    }
    
    SomeHighLevelService service = new SomeHighLevelService();
    service.doFooBar();
  2. Plus proche de SRP, pas d'IoC

    class SomeHighLevelService
    {
        public SomeHighLevelService(){
            Foo foo = new Foo();
            Bar bar = new Bar();
        }
    
        public doFooBar(){
            Crap crap = foo.doFoo();
            bar.doBar(crap);
        }
    }
    
    class Foo {
        public Crap doFoo(){
            //...
            return crap;
        }
    }
    
    class Bar {
        public doBar(Crap crap){
            //...
        }
    }
    
    SomeHighLevelService service = new SomeHighLevelService();
    service.doFooBar();
  3. Oui SRP, non IoC

    class HighLevelServiceProvider {
        public SomeHighLevelService getSomeHighLevelService(){
            SomeHighLevelService service = new SomeHighLevelService();
            service.setFoo(this.getFoo());
            service.getBar(this.getBar());
            return service;
        }
    
        private Foo getFoo(){
            return new Foo();
        }
    
        private Bar getBar(){
            return new Bar();
        }
    }
    
    class SomeHighLevelService
    {           
        public setFoo(Foo foo){
            this.foo = foo;
        }
    
        public setBar(Bar bar){
            this.bar = bar;
        }
    
        public doFooBar(){
            Crap crap = foo.doFoo();
            bar.doBar(crap);
        }
    
    }
    
    class Foo {
        public Crap doFoo(){
            //...
            return crap;
        }
    }
    
    class Bar {
        public doBar(Crap crap){
            //...
        }
    }
    
    HighLevelServiceProvider provider = new HighLevelServiceProvider();
    SomeHighLevelService service = provider.getSomeHighLevelService();
    service.doFooBar();
  4. Oui SRP, oui IoC

    interface HighLevelServiceProvider {
        SomeHighLevelService getSomeHighLevelService();
    }
    
    interface SomeHighLevelService {
        doFooBar();
    }
    
    interface Foo {
        Crap doFoo();
    }
    
    interface Bar {
        doBar(Crap crap);
    }
    
    
    class ConcreteHighLevelServiceContainer implements HighLevelServiceProvider {
        public SomeHighLevelService getSomeHighLevelService(){
            SomeHighLevelService service = new ConcreteHighLevelService();
            service.setFoo(this.getFoo());
            service.getBar(this.getBar());
            return service;
        }
    
        private Foo getFoo(){
            return new ConcreteFoo();
        }
    
        private Bar getBar(){
            return new ConcreteBar();
        }
    }
    
    class ConcreteHighLevelService implements SomeHighLevelService
    {           
        public setFoo(Foo foo){
            this.foo = foo;
        }
    
        public setBar(Bar bar){
            this.bar = bar;
        }
    
        public doFooBar(){
            Crap crap = foo.doFoo();
            bar.doBar(crap);
        }
    
    }
    
    class ConcreteFoo implements Foo {
        public Crap doFoo(){
            //...
            return crap;
        }
    }
    
    class ConcreteBar implements Bar {
        public doBar(Crap crap){
            //...
        }
    }
    
    
    HighLevelServiceProvider provider = new ConcreteHighLevelServiceContainer();
    SomeHighLevelService service = provider.getSomeHighLevelService();
    service.doFooBar();

Nous avons donc fini par avoir un code dans lequel vous pouvez remplacer chaque implémentation concrète par une autre qui implémente la même interface ofc. C'est donc bien car les classes participantes sont découplées les unes des autres, elles ne connaissent que les interfaces. Autre avantage que le code de l'instanciation est réutilisable.

inf3rno
la source