Ricevo un'iniezione di dipendenza, ma qualcuno può aiutarmi a capire la necessità di un contenitore IoC?

15

Mi scuso se questa sembra l'ennesima ripetizione della domanda, ma ogni volta che trovo un articolo riguardante l'argomento, si parla principalmente di DI. Quindi, ottengo DI, ma sto cercando di capire la necessità di un contenitore IoC, in cui tutti sembrano entrare. Il punto di un contenitore IoC è davvero solo per "auto-risolvere" l'implementazione concreta delle dipendenze? Forse le mie classi tendono a non avere più dipendenze e forse è per questo che non vedo il grosso problema, ma voglio essere certo di capire l'utilità del contenitore correttamente.

In genere interrompo la mia logica di business in una classe che potrebbe essere simile a questa:

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

Quindi ha solo una dipendenza, e in alcuni casi potrebbe avere un secondo o un terzo, ma non così spesso. Quindi tutto ciò che chiama questo può passare il suo repository o permetterlo di usare il repository predefinito.

Per quanto riguarda la mia attuale comprensione di un contenitore IoC, tutto ciò che fa il container è la risoluzione di IDataRepository. Ma se è tutto ciò che fa, allora non vedo un sacco di valore in esso poiché le mie classi operative già definiscono un fallback quando nessuna dipendenza è passata. Quindi l'unico altro beneficio che posso pensare è che se ho diverse operazioni come questo usa lo stesso repository di fallback, posso cambiare quel repository in un posto che è il registro / factory / container. E questo è grandioso, ma è così?

    
posta Sinaesthetic 19.09.2014 - 01:49
fonte

3 risposte

2

Il contenitore IoC non riguarda il caso in cui si ha una dipendenza. Si tratta del caso in cui si hanno 3 dipendenze e hanno diverse dipendenze che hanno dipendenze ecc.

Aiuta anche a centralizzare la risoluzione di una dipendenza e la gestione del ciclo di vita delle dipendenze.

    
risposta data 20.09.2014 - 01:07
fonte
10

Esistono diversi motivi per cui potresti voler utilizzare un contenitore IoC.

DLL non referenziate

È possibile utilizzare un contenitore IoC per risolvere una classe concreta da una dll non referenziata. Ciò significa che puoi prendere dipendenze interamente dall'astrazione, cioè dall'interfaccia.

Evita l'uso di new

Un contenitore IoC significa che puoi rimuovere completamente l'uso della parola chiave new per creare una classe. Questo ha due effetti. Il primo è che disaccoppia le tue classi. Il secondo (che è relativo) è che è possibile abbandonare i mock per il test dell'unità. Questo è incredibilmente utile, in particolare quando si interagisce con un processo di lunga durata.

Scrivi contro le astrazioni

L'uso di un contenitore IoC per risolvere dipendenze concrete per te ti consente di scrivere il tuo codice contro le astrazioni, piuttosto che implementare ogni classe concreta di cui hai bisogno quando ne hai bisogno. Ad esempio, potrebbe essere necessario il codice per leggere i dati da un database. Invece di scrivere la classe di interazione del database, si scrive semplicemente un'interfaccia per esso e il codice contro quello. Puoi utilizzare una simulazione per testare la funzionalità del codice che stai sviluppando mentre lo stai sviluppando, piuttosto che basarti sullo sviluppo della concreta classe di interazione del database prima di poter testare l'altro codice.

Evita codice fragile

Un altro motivo per usare un contenitore IoC è che, facendo affidamento sul contenitore IoC per risolvere le dipendenze, si evita la necessità di cambiare ogni singola chiamata a un costruttore di classi quando si aggiunge o si rimuove una dipendenza. Il contenitore IoC risolverà automaticamente le tue dipendenze. Questo non è un grosso problema quando crei una classe una volta, ma è un problema enorme quando crei la classe in cento luoghi.

Gestione a vita e pulizia del reso non gestito

L'ultima ragione che menzionerò è la gestione delle vite degli oggetti. I contenitori IoC spesso forniscono la possibilità di specificare la durata di un oggetto. Ha molto senso specificare la durata di un oggetto in un contenitore IoC piuttosto che cercare di gestirlo manualmente nel codice. La gestione manuale della durata può essere molto difficile. Questo può essere utile quando si tratta di oggetti che richiedono lo smaltimento. Invece di gestire manualmente lo smaltimento dei tuoi oggetti, alcuni contenitori IoC gestiranno lo smaltimento per te, che può aiutare a prevenire perdite di memoria e semplificare la base di codice.

Il problema con il codice di esempio che hai fornito è che la classe che stai scrivendo ha una dipendenza concreta dalla classe ConcreteRepository. Un contenitore IoC rimuove tale dipendenza.

    
risposta data 19.09.2014 - 02:31
fonte
2

Secondo il principio della responsabilità unica ogni classe deve avere una sola responsabilità. Creare nuove istanze di classi è solo un'altra responsabilità, quindi devi incapsulare questo tipo di codice in una o più classi. Puoi farlo utilizzando qualsiasi schema di creazione, ad esempio fabbriche, costruttori, contenitori DI ecc.

Ci sono altri principi come l'inversione del controllo e l'inversione di dipendenza. In questo contesto sono correlati all'istanziazione delle dipendenze. Affermano che le classi di alto livello devono essere disaccoppiate dalle classi di basso livello (dipendenze) che usano. Possiamo disaccoppiare le cose creando interfacce. Quindi le classi di basso livello devono implementare interfacce specifiche e le classi di alto livello devono utilizzare istanze di classi che implementano queste interfacce. (nota: il vincolo di interfaccia uniforme REST applica lo stesso approccio a livello di sistema.)

La combinazione di questi principi con l'esempio (mi dispiace per il codice di bassa qualità, ho usato un linguaggio ad hoc al posto di C #, poiché non lo so):

  1. No SRP, no 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. Più vicino a SRP, no 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. Sì SRP, no 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. Sì SRP, sì 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();
    

Quindi abbiamo finito per avere un codice in cui è possibile sostituire ogni implementazione concreta con un'altra che implementa la stessa interfaccia ofc. Quindi questo è un bene perché le classi partecipanti sono disaccoppiate l'una dall'altra, conoscono solo le interfacce. Un altro vantaggio è che il codice dell'istanza è riutilizzabile.

    
risposta data 19.09.2014 - 04:38
fonte