Comprensione dell'iniezione di dipendenza


109

Sto leggendo l' iniezione di dipendenza (DI). Per me, è una cosa molto complicata da fare, dato che stavo leggendo si riferiva anche all'inversione del controllo (IoC) e tale mi sembrava di essere in viaggio.

Questa è la mia comprensione: invece di creare un modello nella classe che lo consuma anche, si passa (si inietta) il modello (già pieno di proprietà interessanti) a dove è necessario (a una nuova classe che potrebbe portarlo come parametro in il costruttore).

Per me, questo è solo passare un argomento. Devo aver perso la comprensione del punto? Forse diventa più ovvio con progetti più grandi?

La mia comprensione è non DI (utilizzando lo pseudo codice):

public void Start()
{
    MyClass class = new MyClass();
}

...

public MyClass()
{
    this.MyInterface = new MyInterface(); 
}

E DI sarebbe

public void Start()
{
    MyInterface myInterface = new MyInterface();
    MyClass class = new MyClass(myInterface);
}

...

public MyClass(MyInterface myInterface)
{
    this.MyInterface = myInterface; 
}

Qualcuno potrebbe fare luce perché sono sicuro di essere in una confusione qui.


5
Prova ad avere 5 di MyInterface e MyClass E avere più classi che formano una catena di dipendenze. Diventa un dolore nel sedere per impostare tutto.
Euforico

159
" Per me, questo è solo passare una discussione. Devo aver frainteso il punto? " No. Avete capito bene. Questa è "iniezione di dipendenza". Ora vedi quale altro gergo pazzo puoi inventare per concetti semplici, è divertente!
Eric Lippert,

8
Non posso esprimere abbastanza il commento dell'onorevole Lippert. Anch'io ho scoperto che era un gergo confuso per qualcosa che stavo facendo naturalmente, comunque.
Alex Humphrey,

16
@EricLippert: se corretto, penso che sia leggermente riduttivo. Parametri-come-DI non è un concetto immediatamente chiaro per il tuo programmatore imperativo / OO medio, che è abituato a trasmettere dati. DI qui ti sta lasciando passare il comportamento , il che richiede una visione del mondo più flessibile di quella che avrà un programmatore mediocre.
Phoshi,

14
@Phoshi: Hai un buon punto, ma hai anche messo il dito sul perché sono scettico che "l'iniezione di dipendenza" sia una buona idea in primo luogo. Se scrivo una classe che dipende dalla sua correttezza e prestazioni dal comportamento di un'altra classe, l' ultima cosa che voglio fare è rendere l'utente della classe responsabile della corretta costruzione e configurazione della dipendenza! Questo è il mio lavoro. Ogni volta che permetti a un consumatore di iniettare una dipendenza, crei un punto in cui il consumatore potrebbe sbagliare.
Eric Lippert,

Risposte:


106

Bene sì, inietti le tue dipendenze, attraverso un costruttore o tramite proprietà.
Uno dei motivi di ciò non è ingombrare MyClass con i dettagli di come deve essere costruita un'istanza di MyInterface. MyInterface potrebbe essere qualcosa che ha un intero elenco di dipendenze da solo e il codice di MyClass diventerebbe brutto molto velocemente se avessi istanziato tutte le dipendenze di MyInterface all'interno di MyClass.

Un altro motivo è per il test.
Se si ha una dipendenza su un'interfaccia del lettore di file e si inietta questa dipendenza attraverso un costruttore in, diciamo, ConsumerClass, ciò significa che durante il test, è possibile passare un'implementazione in memoria del lettore di file a ConsumerClass, evitando la necessità di eseguire I / O durante il test.


13
Questo è fantastico, ben spiegato. Accetta un +1 immaginario poiché il mio rappresentante non lo consente ancora!
MyDaftQuestions

11
Il complicato scenario costruttivo è un buon candidato per l'uso del modello Factory. In questo modo, la fabbrica si assume la responsabilità di costruire l'oggetto, in modo che la classe stessa non sia obbligata e tu rimanga fedele al principio della singola responsabilità.
Matt Gibson,

1
@MattGibson buon punto.
Stefan Billiet,

2
+1 per i test, un DI ben strutturato aiuterà sia ad accelerare i test sia a realizzare unit test all'interno della classe che stai testando.
Umur Kontacı,

52

Il modo in cui DI può essere implementato dipende molto dalla lingua utilizzata.

Ecco un semplice esempio non DI:

class Foo {
    private Bar bar;
    private Qux qux;

    public Foo() {
        bar = new Bar();
        qux = new Qux();
    }
}

Questo fa schifo, ad esempio quando per un test voglio usare un oggetto finto per bar. Quindi possiamo renderlo più flessibile e consentire il passaggio delle istanze tramite il costruttore:

class Foo {
    private Bar bar;
    private Qux qux;

    public Foo(Bar bar, Qux qux) {
        this.bar = bar;
        this.qux = qux;
    }
}

// in production:
new Foo(new Bar(), new Qux());
// in test:
new Foo(new BarMock(), new Qux());

Questa è già la forma più semplice di iniezione di dipendenza. Ma questo fa ancora schifo perché tutto deve essere fatto manualmente (anche perché il chiamante può contenere un riferimento ai nostri oggetti interni e quindi invalidare il nostro stato).

Possiamo introdurre più astrazioni usando le fabbriche:

  • Un'opzione è quella Foodi essere generato da una fabbrica astratta

    interface FooFactory {
        public Foo makeFoo();
    }
    
    class ProductionFooFactory implements FooFactory {
        public Foo makeFoo() { return new Foo(new Bar(), new Baz()) }
    }
    
    class TestFooFactory implements FooFactory {
        public Foo makeFoo() { return new Foo(new BarMock(), new Baz()) }
    }
    
    FooFactory fac = ...; // depends on test or production
    Foo foo = fac.makeFoo();
  • Un'altra opzione è passare una fabbrica al costruttore:

    interface DependencyManager {
        public Bar makeBar();
        public Qux makeQux();
    }
    class ProductionDM implements DependencyManager {
        public Bar makeBar() { return new Bar() }
        public Qux makeQux() { return new Qux() }
    }
    class TestDM implements DependencyManager {
        public Bar makeBar() { return new BarMock() }
        public Qux makeQux() { return new Qux() }
    }
    
    class Foo {
        private Bar bar;
        private Qux qux;
    
        public Foo(DependencyManager dm) {
            bar = dm.makeBar();
            qux = dm.makeQux();
        }
    }

I restanti problemi con questo sono che dobbiamo scrivere una nuova DependencyManagersottoclasse per ogni configurazione e che il numero di dipendenze che possono essere gestite è abbastanza limitato (ogni nuova dipendenza necessita di un nuovo metodo nell'interfaccia).

Con caratteristiche come la riflessione e il caricamento dinamico delle classi possiamo aggirare questo. Ma questo dipende molto dalla lingua utilizzata. In Perl, alle classi si può fare riferimento con il loro nome, e io potrei semplicemente farlo

package Foo {
    use signatures;

    sub new($class, $dm) {
        return bless {
            bar => $dm->{bar}->new,
            qux => $dm->{qux}->new,
        } => $class;
    }
}

my $prod = { bar => 'My::Bar', qux => 'My::Qux' };
my $test = { bar => 'BarMock', qux => 'QuxMock' };
$test->{bar} = 'OtherBarMock';  # change conf at runtime

my $foo = Foo->new(rand > 0.5 ? $prod : $test);

In lingue come Java, il gestore delle dipendenze potrebbe comportarsi in modo simile a Map<Class, Object>:

Bar bar = dm.make(Bar.class);

A quale classe effettiva Bar.classviene risolto può essere configurato in fase di esecuzione, ad esempio mantenendo un Map<Class, Class>quale mappa le interfacce alle implementazioni.

Map<Class, Class> dependencies = ...;

public <T> T make(Class<T> c) throws ... {
    // plus a lot more error checking...
    return dependencies.get(c).newInstance();
}

C'è ancora un elemento manuale coinvolto nella scrittura del costruttore. Ma possiamo rendere il costruttore completamente inutile, ad esempio guidando il DI tramite annotazioni:

class Foo {
    @Inject(Bar.class)
    private Bar bar;

    @Inject(Qux.class)
    private Qux qux;

    ...
}

dm.make(Foo.class);  // takes care of initializing "bar" and "qux"

Ecco un esempio di implementazione di un piccolo (e molto limitato) framework DI: http://ideone.com/b2ubuF , sebbene questa implementazione sia completamente inutilizzabile per oggetti immutabili (questa implementazione ingenua non può accettare alcun parametro per il costruttore).


7
Questo è super con ottimi esempi, anche se mi ci vorranno alcune volte a leggerlo per digerire tutto, ma grazie per aver dedicato così tanto tempo.
MyDaftQuestions

33
È divertente come ogni prossima semplificazione sia più complessa
Kromster

48

I nostri amici su Stack Overflow hanno una bella risposta a questo . La mia preferita è la seconda risposta, compresa la citazione:

"Iniezione delle dipendenze" è un termine di 25 dollari per un concetto da 5 cent. (...) Iniezione di dipendenza significa dare a un oggetto le sue variabili di istanza. (...).

dal blog di James Shore . Naturalmente, ci sono versioni / schemi più complessi sovrapposti a questo, ma è abbastanza per capire fondamentalmente cosa sta succedendo. Invece di un oggetto che crea le proprie variabili di istanza, vengono passati dall'esterno.


10
Avevo letto questo ... e fino ad ora, non aveva senso ... Ora, ha senso. La dipendenza non è in realtà un concetto così grande da capire (indipendentemente da quanto sia potente) ma ha un grande nome e un sacco di rumore su Internet ad esso associato!
MyDaftQuestions

18

Supponiamo che tu stia facendo il design degli interni nel tuo salotto: installi un lampadario di fantasia sul soffitto e inserisci una lampada da terra abbinata di classe. Un anno dopo la tua adorabile moglie decide che le piacerebbe un po 'meno la stanza. Quale apparecchio di illuminazione sarà più facile da cambiare?

L'idea alla base di DI è di utilizzare l'approccio "plug-in" ovunque. Questo potrebbe non sembrare un grosso problema se vivi in ​​una semplice baracca senza muro a secco e tutti i cavi elettrici esposti. (Sei un tuttofare - puoi cambiare qualsiasi cosa!) E allo stesso modo, su un'applicazione piccola e semplice, DI può aggiungere più complicazioni di quanto valga la pena.

Ma per un'applicazione ampia e complessa, DI è indispensabile per la manutenzione a lungo termine. Ti permette di "scollegare" interi moduli dal tuo codice (il backend del database, per esempio), e di scambiarli con diversi moduli che realizzano la stessa cosa ma lo fanno in un modo completamente diverso (un sistema di archiviazione cloud, ad esempio).

A proposito, una discussione su DI sarebbe incompleta senza una raccomandazione di Dependency Injection in .NET , di Mark Seeman. Se hai familiarità con .NET e hai mai intenzione di essere coinvolto nello sviluppo di software su larga scala, questa è una lettura essenziale. Spiega il concetto molto meglio di me.

Lascia che ti lasci un'ultima caratteristica essenziale di DI che il tuo esempio di codice trascura. DI ti consente di sfruttare il vasto potenziale di flessibilità che è racchiuso nell'adagio " Programma alle interfacce ". Per modificare leggermente il tuo esempio:

public void Main()
{
    ILightFixture fixture = new ClassyChandelier();
    MyRoom room = new MyRoom (fixture);
}
...
public MyRoom(ILightFixture fixture)
{
    this.MyLightFixture = fixture ; 
}

Ma ADESSO, poiché MyRoomè progettato per l' interfaccia ILightFixture , posso facilmente passare l'anno prossimo e cambiare una riga nella funzione principale ("iniettare" una "dipendenza" diversa) e ottenere immediatamente nuove funzionalità in MyRoom (senza dover ricostruire o ridistribuire MyRoom, se si trova in una libreria separata, presupponendo, naturalmente, che tutte le implementazioni di ILightFixture siano progettate tenendo presente la sostituzione di Liskov). Tutto, con un semplice cambiamento:

public void Main()
{
    ILightFixture fixture = new MuchLessFormalLightFixture();
    MyRoom room = new MyRoom (fixture);
}

In più, ora posso Test Unit MyRoom(si è unit test, giusto?) E utilizzare una lampada "finto", che mi permette di testare MyRoomtutto indipendenteClassyChandelier.

Ci sono molti altri vantaggi, ovviamente, ma questi sono quelli che mi hanno venduto l'idea.


Volevo dire grazie, mi è stato utile, ma non vogliono che tu lo faccia, ma piuttosto usano i commenti per suggerire miglioramenti, quindi, se ne hai voglia, potresti aggiungere uno degli altri vantaggi che hai menzionato. Ma per il resto, grazie, mi è stato utile.
Steve

2
Sono anche interessato al libro a cui hai fatto riferimento, ma trovo allarmante che ci sia un libro di 584 pagine su questo argomento.
Steve

4

L'iniezione di dipendenza sta semplicemente passando un parametro, ma ciò porta ancora ad alcuni problemi e il nome è destinato a focalizzarsi un po 'sul perché la differenza tra la creazione di un oggetto e il passaggio in uno è importante.

È importante perché (ovviamente) ogni volta che l'oggetto A chiama il tipo B, A dipende da come funziona B. Ciò significa che se A prende la decisione su quale tipo concreto di B utilizzerà, allora si è persa molta flessibilità nel modo in cui A può essere usato da classi esterne, senza modificare A.

Ne parlo perché parli di "perdere il punto" dell'iniezione di dipendenza, e questo è in gran parte il motivo per cui alla gente piace farlo. Può significare semplicemente passare un parametro, ma se lo fai può essere importante.

Inoltre, alcune delle difficoltà nell'attuazione effettiva del DI si manifestano meglio nei grandi progetti. In genere sposterai la decisione effettiva su quali classi concrete utilizzare fino in fondo, quindi il tuo metodo di avvio potrebbe apparire come:

public void Start()
{
    MySubSubInterfaceA mySubSubInterfaceA = new mySubSubInterfaceA();
    MySubSubInterfaceB mySubSubInterfaceB = new mySubSubInterfaceB();     
    MySubInterface mySubInterface = new MySubInterface(mySubSubInterfaceA,mySubSubInterfaceB);
    MyInterface myInterface = new MyInterface(MySubInterface);
    MyClass class = new MyClass(myInterface);
}

ma con altre 400 righe di questo tipo di codice di rivettatura. Se immagini di mantenerlo nel tempo, l'appeal dei contenitori DI diventa più evidente.

Inoltre, immagina una classe che, per esempio, implementa IDisposable. Se le classi che lo usano lo ottengono tramite l'iniezione anziché crearlo da soli, come fanno a sapere quando chiamare Dispose ()? (Che è un altro problema che tratta un contenitore DI.)

Quindi, certo, l'iniezione di dipendenza sta semplicemente passando un parametro, ma in realtà quando le persone dicono che l'iniezione di dipendenza significa "passare un parametro al tuo oggetto rispetto all'alternativa di far istanziare qualcosa da solo", e gestire tutti i vantaggi degli svantaggi di tale approccio, possibilmente con l'aiuto di un contenitore DI.


Sono molti sottotitoli e interfacce! Intendi rinnovare un'interfaccia anziché una classe che implementa l'interfaccia? Sembra sbagliato
JBR Wilkinson,

@JBRWilkinson Intendo una classe che implementa un'interfaccia. Dovrei chiarirlo. Tuttavia, il punto è che un'app di grandi dimensioni avrà classi con dipendenze che a loro volta hanno dipendenze che a loro volta hanno dipendenze ... L'impostazione manuale del metodo principale è il risultato di un'iniezione di molti costruttori.
ps

4

A livello di classe, è facile.

"Dependency Injection" risponde semplicemente alla domanda "come troverò i miei collaboratori" con "sono spinti verso di te - non devi andare a prenderli tu stesso". (È simile a - ma non uguale a - "Inversion of Control" in cui la domanda "come ordinerò le mie operazioni sui miei input?" Ha una risposta simile).

L' unico vantaggio che ti spinge i tuoi collaboratori è che consente al codice client di utilizzare la tua classe per comporre un grafico di oggetti che si adatta alle sue attuali esigenze ... non hai arbitrariamente predeterminato la forma e la mutabilità del grafico decidendo privatamente i tipi concreti e il ciclo di vita dei tuoi collaboratori.

(Tutti gli altri vantaggi, la testabilità, l'accoppiamento lento, ecc., Derivano in gran parte dall'uso delle interfacce e non tanto dall'iniezione di dipendenza, sebbene DI promuova naturalmente l'uso di interfacce).

Vale la pena notare che, se eviti di creare un'istanza per i tuoi collaboratori, la tua classe deve quindi ottenere i suoi collaboratori da un costruttore, una proprietà o un argomento di metodo (quest'ultima opzione viene spesso trascurata, tra l'altro ... non sempre ha senso che i collaboratori di una classe facciano parte del suo "stato").

E questa è una buona cosa.

A livello di applicazione ...

Questo per quanto riguarda la vista per classe delle cose. Supponiamo che tu abbia un gruppo di classi che seguono la regola "non creare un'istanza per i tuoi collaboratori" e desiderano farne richiesta. La cosa più semplice da fare è usare un buon vecchio codice (uno strumento davvero utile per invocare costruttori, proprietà e metodi!) Per comporre il grafico a oggetti desiderato e lanciarne l'input. (Sì, alcuni di quegli oggetti nel tuo grafico saranno essi stessi fabbriche di oggetti, che sono stati passati come collaboratori ad altri oggetti di lunga durata nel grafico, pronti per il servizio ... non puoi precostruire ogni oggetto! ).

... la necessità di configurare "in modo flessibile" il grafico degli oggetti della tua app ...

A seconda dei tuoi altri obiettivi (non relativi al codice), potresti voler dare all'utente finale un certo controllo sul grafico degli oggetti così distribuito. Questo ti porta nella direzione di uno schema di configurazione, di un tipo o di un altro, che si tratti di un file di testo di tua progettazione con alcune coppie nome / valore, un file XML, un DSL personalizzato, un linguaggio dichiarativo grafico-descrizione come YAML, un linguaggio di scripting imperativo come JavaScript o qualcos'altro appropriato all'attività da svolgere. Tutto ciò che serve per comporre un grafico a oggetti valido, in modo tale da soddisfare le esigenze dei tuoi utenti.

... può essere una forza progettuale significativa.

Nelle circostanze più estreme di quel tipo, potresti scegliere di adottare un approccio molto generale e fornire agli utenti finali un meccanismo generale per "cablare" il grafico oggetto di loro scelta e persino consentire loro di fornire realizzazioni concrete di interfacce al runtime! (La tua documentazione è un gioiello luccicante, i tuoi utenti sono molto intelligenti, hanno familiarità con almeno il profilo approssimativo del grafico a oggetti dell'applicazione, ma non ti capita di avere un compilatore a portata di mano). Questo scenario può teoricamente verificarsi in alcune situazioni "aziendali".

In tal caso, probabilmente hai un linguaggio dichiarativo che consente ai tuoi utenti di esprimere qualsiasi tipo, la composizione di un grafico a oggetti di tali tipi e una tavolozza di interfacce che il mitico utente finale può mescolare e abbinare. Per ridurre il carico cognitivo sui tuoi utenti, preferisci un approccio di "configurazione per convenzione", in modo che debbano solo intervenire e superare il frammento di oggetto-grafico di interesse, invece di lottare con il tutto.

Povera zolla!

Poiché non ti andava di scrivere tutto da solo (ma seriamente, dai un'occhiata a un binding YAML per la tua lingua), stai usando un framework DI di qualche tipo.

A seconda della maturità di quel framework, potresti non avere la possibilità di usare l'iniezione del costruttore, anche quando ha senso (i collaboratori non cambiano nel corso della vita di un oggetto), costringendoti così a usare Setter Injection (anche quando i collaboratori non cambiano nel corso della vita di un oggetto e anche quando non c'è davvero una ragione logica per cui tutte le implementazioni concrete di un'interfaccia devono avere collaboratori di un tipo specifico). In tal caso, sei attualmente in un inferno di accoppiamento forte, nonostante abbia diligentemente "usato le interfacce" in tutta la tua base di codice - horror!

Spero comunque che tu abbia usato un framework DI che ti dà la possibilità di iniezione del costruttore, e i tuoi utenti sono solo un po 'scontrosi con te per non passare più tempo a pensare alle cose specifiche che hanno bisogno di configurare e dare loro un'interfaccia utente più adatta all'attività a mano. (Anche se per essere onesti, probabilmente hai provato a pensare a un modo, ma JavaEE ti ha deluso e hai dovuto ricorrere a questo orribile hack).

Bootnote

Non usi mai Google Guice, che ti offre al programmatore un modo per rinunciare al compito di comporre un oggetto-grafico con il codice ... scrivendo il codice. Argh!


0

Molte persone hanno affermato che DI è un meccanismo per esternalizzare l'inizializzazione di una variabile di istanza.

Per esempi ovvi e semplici non hai davvero bisogno di "iniezione di dipendenza". Puoi farlo con un semplice parametro passando al costruttore, come hai notato in questione.

Tuttavia, penso che un meccanismo dedicato di "iniezione di dipendenza" sia utile quando parli di risorse a livello di applicazione, in cui sia il chiamante che il chiamante non sanno nulla di queste risorse (e non vogliono saperlo!).

Ad esempio: connessione al database, contesto di sicurezza, funzione di registrazione, file di configurazione ecc.

Inoltre, in generale, ogni singolo è un buon candidato per DI. Più ne hai e usi, più possibilità avrai di DI.

Per questo tipo di risorse è abbastanza chiaro che non ha senso spostarli tutti attraverso liste di parametri e quindi il DI è stato inventato.

L'ultima nota, è necessario anche un contenitore DI, che eseguirà l'iniezione effettiva ... (di nuovo, per rilasciare sia il chiamante che il destinatario dai dettagli di implementazione).


-2

Se si passa a un tipo di riferimento astratto, il codice chiamante può passare in qualsiasi oggetto che estende / implementa il tipo astratto. Quindi in futuro la classe che utilizza l'oggetto potrebbe usare un nuovo oggetto che è stato creato senza mai modificare il suo codice.


-4

Questa è un'altra opzione oltre alla risposta di Amon

Usa costruttori:

classe Foo {
    Bar privato;
    Qux qux privato;

    private Foo () {
    }

    public Foo (Builder builder) {
        this.bar = builder.bar;
        this.qux = builder.qux;
    }

    Builder pubblico di classe statica {
        Bar privato;
        Qux qux privato;
        public Buider setBar (Bar bar) {
            this.bar = bar;
            restituire questo;
        }
        public Builder setQux (Qux qux) {
            this.qux = qux;
            restituire questo;
        }
        public Foo build () {
            ritorna nuovo Foo (questo);
        }
    }
}

// in produzione:
Foo foo = new Foo.Builder ()
    .setBar (new Bar ())
    .setQux (nuovo Qux ())
    .costruire();

// in prova:
Foo testFoo = new Foo.Builder ()
    .setBar (nuovo BarMock ())
    .setQux (nuovo Qux ())
    .costruire();

Questo è quello che uso per le dipendenze. Non uso alcun framework DI. Si mettono in mezzo.


4
Questo non aggiunge molto alle altre risposte di un anno fa e non offre alcuna spiegazione del perché un costruttore potrebbe aiutare chi chiede a capire l'iniezione di dipendenza.

@Snowman È un'altra opzione al posto di DI. Mi dispiace che tu non la veda così. Grazie per il voto negativo.
user3155341

1
il richiedente voleva capire se DI fosse davvero semplice come passare un parametro, non chiedeva alternative a DI.

1
Gli esempi dell'OP sono molto validi. I tuoi esempi forniscono una maggiore complessità per qualcosa che è un'idea semplice. Non illustrano il problema della domanda.
Adam Zuckerman,

Il costruttore DI sta passando un parametro. Tranne il fatto che stai passando un oggetto interfaccia piuttosto che un oggetto classe concreto. È questa "dipendenza" da un'implementazione concreta che viene risolta tramite DI. Il costruttore non sa che tipo viene passato, né gli importa, sa solo che sta ricevendo un tipo che implementa un'interfaccia. Suggerisco PluralSight, ha un ottimo corso DI / IOC per principianti.
Hardgraf,
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.