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


15

Mi scuso se questa sembra un'ennesima ripetizione della domanda, ma ogni volta che trovo un articolo sull'argomento, 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 quello di "auto-risolvere" l'implementazione concreta delle dipendenze? Forse le mie classi tendono a non avere diverse dipendenze e forse è per questo che non vedo il grosso problema, ma voglio assicurarmi di capire correttamente l'utilità del contenitore.

In genere divido la mia logica aziendale in una classe che potrebbe assomigliare 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 una seconda o una terza, ma non così spesso. Quindi tutto ciò che chiama questo può passare il proprio repository o consentirgli di utilizzare il repository predefinito.

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


1
Spesso avere una versione di fallback predefinita della dipendenza non ha davvero senso.
Ben Aaronson,

In che modo vuoi dire? Il "fallback" è la classe concreta che viene utilizzata praticamente tutto il tempo tranne nei test unitari. In effetti, questa sarebbe la stessa classe registrata nel contenitore.
Sinaesthetic,

Sì, ma con il contenitore: (1) tutti gli altri oggetti nel contenitore ottengono la stessa istanza di ConcreteRepositorye (2) è possibile fornire dipendenze aggiuntive ConcreteRepository(una connessione al database sarebbe comune, ad esempio).
Jules,

@Sinaestetico Non intendo che sia sempre una cattiva idea, ma spesso non è appropriato. Ad esempio, ti impedirebbe di seguire l'architettura della cipolla con i riferimenti del tuo progetto. Inoltre, potrebbe non esserci una chiara implementazione predefinita. E come dice Jules, i contenitori IOC riescono non solo a scegliere il tipo di dipendenza, ma fanno cose come la condivisione di istanze e la gestione del ciclo di vita
Ben Aaronson,

Vado a fare una maglietta che recita "Parametri funzionali - L'iniezione di dipendenza ORIGINALE!"
Graham,

Risposte:


2

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

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


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.

Evitare l'uso di new

Un contenitore IoC significa che è possibile rimuovere completamente l'uso della newparola chiave per creare una classe. Questo ha due effetti. Il primo è che disaccoppia le tue lezioni. Il secondo (che è correlato) è che puoi lasciarti andare in giro per i test unitari. 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 le 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, devi semplicemente scrivere un'interfaccia per esso e codificarlo. È possibile utilizzare una simulazione per verificare la funzionalità del codice che si sta sviluppando mentre lo si sta sviluppando, piuttosto che fare affidamento sullo sviluppo della classe di interazione del database concreto prima di poter testare l'altro codice.

Evita il codice fragile

Un altro motivo per utilizzare 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 rimuove una dipendenza. Il contenitore IoC risolverà automaticamente le tue dipendenze. Questo non è un grosso problema quando si crea una classe una volta, ma è un problema gigantesco quando si crea la classe in cento posti.

Gestione della vita e pulizia delle risorse non gestita

L'ultima ragione di cui parlerò è la gestione della durata degli oggetti. I contenitori IoC offrono spesso la possibilità di specificare la durata di un oggetto. Ha molto senso specificare la durata di un oggetto in un contenitore IoC piuttosto che provare a 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, il che può aiutarti a prevenire perdite di memoria e semplificare la tua 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 eliminerebbe tale dipendenza.


22
Questi non sono vantaggi dei contenitori IoC, sono vantaggi dell'iniezione di dipendenza, che può essere fatta facilmente con il DI del povero
Ben Aaronson,

Scrivere un buon codice DI senza un contenitore IoC può effettivamente essere piuttosto difficile. Sì, c'è qualche sovrapposizione nei vantaggi, ma questi sono tutti i vantaggi meglio sfruttati con un contenitore IoC.
Stephen,

Bene, gli ultimi due motivi che hai aggiunto dopo il mio commento sono più specifici del container e sono entrambi argomenti molto forti secondo me
Ben Aaronson,

"Evita l'uso di nuovi" - ostacola anche l'analisi del codice statico, quindi devi iniziare a usare qualcosa del genere: hmemcpy.github.io/AgentMulder . Altri vantaggi descritti in questo paragrafo riguardano i DI e non l'IoC. Inoltre, le classi verranno comunque accoppiate se si evita di utilizzare nuove, ma verranno utilizzati tipi concreti anziché interfacce per i parametri.
Den

1
Nel complesso dipinge l'IoC come qualcosa senza difetti, per esempio non si fa menzione del grande svantaggio: spingere una classe di errori nel runtime anziché in fase di compilazione.
Den

2

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

Esistono altri principi come l'inversione del controllo e l'inversione della dipendenza. In questo contesto sono collegati all'istanza delle dipendenze. Dichiarano 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 dell'interfaccia uniforme REST applica lo stesso approccio a livello di sistema.)

La combinazione di questi principi per esempio (scusate il codice di bassa qualità, ho usato un linguaggio ad-hoc invece di C #, poiché non lo so):

  1. Nessun SRP, nessun 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 separate l'una dall'altra, conoscono solo le interfacce. Un altro vantaggio è che il codice dell'istanza è riutilizzabile.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.