c# - Perché si usa l'iniezione di dipendenza?


Sto cercando di capire le iniezioni di dipendenza (DI), e ancora una volta ho fallito. Sembra solo sciocco. Il mio codice non è mai un casino; Scrivo a malapena funzioni e interfacce virtuali (anche se lo faccio una volta in una luna blu) e tutta la mia configurazione è magicamente serializzata in una classe usando json.net (a volte usando un serializzatore XML).

Non capisco quale problema risolva. Sembra un modo per dire: "ciao. Quando si esegue questa funzione, restituire un oggetto che è di questo tipo e utilizza questi parametri / dati".
Ma ... perché dovrei usarlo? Nota Non ho mai avuto bisogno di usare l' object , ma capisco a cosa serve.

Quali sono alcune situazioni reali nella creazione di un sito Web o di un'applicazione desktop in cui si utilizza DI? Posso trovare facilmente dei casi per cui qualcuno potrebbe voler utilizzare interfacce / funzioni virtuali in un gioco, ma è estremamente raro (abbastanza raro da non ricordare una singola istanza) per usarlo nel codice di gioco.



Answers



Per prima cosa, voglio spiegare un'ipotesi che creo per questa risposta. Non è sempre vero, ma abbastanza spesso:

Le interfacce sono aggettivi; le classi sono nomi.

(In realtà, ci sono anche interfacce che sono sostantivi, ma voglio generalizzare qui.)

Quindi, ad esempio un'interfaccia può essere qualcosa come IDisposable , IEnumerable o IPrintable . Una classe è un'implementazione effettiva di una o più di queste interfacce: List o Map possono essere entrambe implementazioni di IEnumerable .

Per ottenere il punto: spesso le tue classi dipendono l'una dall'altra. Ad esempio, potresti avere una classe Database che accede al tuo database (hah, sorpresa! ;-)), ma vuoi anche che questa classe esegua il log dell'accesso al database. Supponiamo che tu abbia un altro Logger classe, quindi Database ha una dipendenza da Logger .

Fin qui tutto bene.

È possibile modellare questa dipendenza all'interno della classe Database con la seguente riga:

var logger = new Logger();

e tutto va bene. Va bene fino al giorno in cui ti rendi conto che hai bisogno di un gruppo di logger: a volte vuoi accedere alla console, a volte al file system, a volte usando TCP / IP e un server di registrazione remoto, e così via ...

E ovviamente NON vuoi cambiare tutto il tuo codice (nel frattempo ne hai gazillions) e sostituire tutte le linee

var logger = new Logger();

di:

var logger = new TcpLogger();

Innanzitutto, non è divertente. In secondo luogo, questo è soggetto a errori. Terzo, questo è un lavoro stupido e ripetitivo per una scimmia addestrata. Allora cosa fai?

Ovviamente è una buona idea introdurre un'interfaccia ICanLog (o simile) implementata da tutti i vari logger. Quindi il punto 1 del tuo codice è che tu faccia:

ICanLog logger = new Logger();

Ora l'inferenza del tipo non cambia più, hai sempre una sola interfaccia da sviluppare contro. Il prossimo passo è che non vuoi avere un new Logger() più e più volte. Quindi metti l'affidabilità per creare nuove istanze in una singola classe centrale e ottieni codice come:

ICanLog logger = LoggerFactory.Create();

La fabbrica stessa decide quale tipo di logger creare. Il tuo codice non si preoccupa più a lungo, e se vuoi cambiare il tipo di logger usato, lo cambi una volta : all'interno della fabbrica.

Ora, ovviamente, puoi generalizzare questo factory e farlo funzionare per qualsiasi tipo:

ICanLog logger = TypeFactory.Create<ICanLog>();

Da qualche parte questo TypeFactory necessita di dati di configurazione che la classe reale istanzia quando viene richiesto un tipo di interfaccia specifico, quindi è necessaria una mappatura. Ovviamente puoi fare questo mapping all'interno del tuo codice, ma un cambio di tipo significa ricompilare. Ma potresti anche inserire questa mappatura all'interno di un file XML, ad es. Questo ti permette di cambiare la classe effettivamente usata anche dopo la compilazione (!), Che significa in modo dinamico, senza ricompilare!

Per dare un esempio utile per questo: Pensa a un software che non si registra normalmente, ma quando il tuo cliente chiama e chiede aiuto perché ha un problema, tutto ciò che gli viene inviato è un file di configurazione XML aggiornato, e ora ha la registrazione è abilitata e il tuo supporto può utilizzare i file di registro per aiutare il cliente.

E ora, quando si sostituiscono un po 'i nomi, si finisce con una semplice implementazione di un Localizzatore di servizio , che è uno dei due modelli di Inversion of Control (dato che si inverte il controllo su chi decide quale classe esatta da istanziare).

Tutto ciò riduce le dipendenze nel codice, ma ora tutto il codice ha una dipendenza dal singolo localizzatore di servizi.

L'iniezione di dipendenza è ora il prossimo passo in questa linea: basta eliminare questa singola dipendenza dal localizzatore di servizio: invece di varie classi che chiedono al localizzatore di servizi un'implementazione per un'interfaccia specifica, tu - ancora una volta - registri il controllo su chi istanzia cosa .

Con l'iniezione di dipendenza, la classe Database ora ha un costruttore che richiede un parametro di tipo ICanLog :

public Database(ICanLog logger) { ... }

Ora il tuo database ha sempre un logger da usare, ma non sa più da dove viene questo logger.

Ed è qui che entra in gioco un framework DI: si configurano nuovamente i mapping e quindi si richiede al framework DI di istanziare l'applicazione per conto dell'utente. Poiché la classe Application richiede un'implementazione ICanPersistData , viene iniettata un'istanza di Database , ma per questo deve prima creare un'istanza del tipo di registratore che è configurato per ICanLog . E così via ...

Quindi, per farla breve: l'iniezione delle dipendenze è uno dei due modi per rimuovere le dipendenze dal codice. È molto utile per le modifiche alla configurazione dopo la compilazione, ed è una grande cosa per il test delle unità (in quanto rende molto facile l'iniezione di stub e / o mock).

In pratica, ci sono cose che non puoi fare senza un localizzatore di servizi (ad esempio, se non sai in anticipo quante istanze hai bisogno di un'interfaccia specifica: un framework DI inietta sempre solo un'istanza per parametro, ma puoi chiamare un localizzatore di servizi all'interno di un loop, ovviamente), quindi più spesso ogni framework DI fornisce anche un localizzatore di servizi.

Ma in fondo, è tutto.

Spero possa aiutare.

PS: Quello che ho descritto qui è una tecnica chiamata constructor injection , c'è anche l' injection di proprietà dove non sono i parametri del costruttore, ma le proprietà vengono utilizzate per definire e risolvere le dipendenze. Pensa all'iniezione di proprietà come dipendenza opzionale e all'iniezione del costruttore come dipendenze obbligatorie. Ma la discussione su questo è oltre lo scopo di questa domanda.




Penso che molte volte le persone si confondano sulla differenza tra l' iniezione di dipendenza e una struttura di iniezione di dipendenza (o un contenitore come viene spesso chiamato).

L'iniezione di dipendenza è un concetto molto semplice. Invece di questo codice:

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

scrivi un codice come questo:

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

E questo è tutto. Sul serio. Questo ti dà un sacco di vantaggi. Due importanti sono la capacità di controllare la funzionalità da un punto centrale (la funzione Main() ) invece di diffonderla per tutto il programma, e la possibilità di testare più facilmente ogni classe in isolamento (perché è possibile passare a mock o altri oggetti falsificati in il suo costruttore invece di un valore reale).

Lo svantaggio, ovviamente, è che ora hai una mega-funzione che conosce tutte le classi usate dal tuo programma. Questo è ciò che i framework DI possono aiutare. Ma se hai difficoltà a capire perché questo approccio è prezioso, ti consiglio di iniziare con l'iniezione manuale delle dipendenze, in primo luogo, in modo da poter meglio apprezzare ciò che i vari framework là fuori possono fare per te.




Come indicato nelle altre risposte, l'iniezione di dipendenza è un modo per creare le dipendenze al di fuori della classe che lo utilizza. Li inietti dall'esterno e prendi il controllo della loro creazione lontano dall'interno della classe. Questo è anche il motivo per cui l'iniezione di dipendenza è una realizzazione del principio di Inversion of control (IoC).

IoC è il principio, dove DI è lo schema. La ragione per cui potresti "aver bisogno di più di un logger" non viene mai realmente soddisfatta, per quanto riguarda la mia esperienza, ma la vera ragione è che ne hai davvero bisogno, ogni volta che provi qualcosa. Un esempio:

La mia caratteristica:

Quando guardo un'offerta, voglio segnare che l'ho guardata automaticamente, in modo che non mi dimentichi di farlo.

Potresti testarlo in questo modo:

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

Quindi da qualche parte nel OfferWeasel , ti costruisce un oggetto di offerta come questo:

public class OfferWeasel
{
    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = DateTime.Now;
        return offer;
    }
}

Il problema qui è che questo test molto probabilmente fallirà sempre, dal momento che la data che viene impostata sarà diversa dalla data che viene asserita, anche se si inserisce semplicemente DateTime.Now nel codice di test potrebbe essere spento di un paio di millisecondi e quindi fallirà sempre. Una soluzione migliore ora sarebbe creare un'interfaccia per questo, che ti permetta di controllare quale sarà il tempo impostato:

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'interfaccia è l'astrazione. Uno è la cosa REALE, e l'altro ti permette di fingere un po 'di tempo dove è necessario. Il test può quindi essere modificato in questo modo:

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

In questo modo, hai applicato il principio dell'inversione del controllo, iniettando una dipendenza (ottenendo l'ora corrente). La ragione principale per fare ciò è semplificare i test delle unità isolate, ci sono altri modi per farlo. Ad esempio, un'interfaccia e una classe qui non sono necessarie poiché in C # le funzioni possono essere passate come variabili, quindi invece di un'interfaccia è possibile utilizzare Func<DateTime> per ottenere lo stesso risultato. Oppure, se si utilizza un approccio dinamico, si passa semplicemente qualsiasi oggetto che abbia il metodo equivalente ( digitazione anatra ) e non è necessaria alcuna interfaccia.

Non avrai quasi mai bisogno di più di un logger. Tuttavia, l'iniezione di dipendenza è essenziale per il codice tipizzato in modo statico come Java o C #.

E ... Va anche notato che un oggetto può solo adempiere correttamente al suo scopo in fase di esecuzione, se tutte le sue dipendenze sono disponibili, quindi non è molto utile impostare l'iniezione di proprietà. Secondo me, tutte le dipendenze dovrebbero essere soddisfatte quando viene chiamato il costruttore, quindi l'iniezione del costruttore è la cosa giusta da fare.

Spero che questo abbia aiutato.




Penso che la risposta classica sia creare un'applicazione più disaccoppiata, che non ha alcuna conoscenza di quale implementazione verrà utilizzata durante il runtime.

Ad esempio, siamo un fornitore di servizi di pagamento centrale, che collabora con molti fornitori di servizi di pagamento in tutto il mondo. Tuttavia, quando viene effettuata una richiesta, non ho idea di quale processore di pagamento chiamerò. Potrei programmare una classe con una tonnellata di casi di switch, come ad esempio:

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

Ora immagina che ora avrai bisogno di mantenere tutto questo codice in una singola classe perché non è disaccoppiato correttamente, puoi immaginare che per ogni nuovo processore che sosterrai, dovrai creare una nuova se ogni metodo, questo diventa solo più complicato, tuttavia, usando Dependency Injection (o Inversion of Control - come viene talvolta chiamato, il che significa che chiunque controlli l'esecuzione del programma è noto solo al runtime, e non alla complicazione), si potrebbe ottenere qualcosa molto pulito e manutenibile.

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

** Il codice non verrà compilato, lo so :)




La ragione principale per usare la DI è che vuoi mettere la responsabilità della conoscenza dell'implementazione dove c'è la conoscenza. L'idea di DI è molto in linea con l'incapsulamento e il design per interfaccia. Se il front-end chiede dal back-end per alcuni dati, non è importante per il front-end come il back-end risolva quella domanda. Questo dipende dal gestore della richiesta.

Questo è già comune in OOP da molto tempo. Molte volte creando pezzi di codice come:

I_Dosomething x = new Impl_Dosomething();

Lo svantaggio è che la classe di implementazione è ancora hardcoded, quindi ha il front end della conoscenza su quale implementazione viene utilizzata. DI prende il design per interfaccia un ulteriore passo avanti, che l'unica cosa che il front-end deve sapere è la conoscenza dell'interfaccia. Tra il DYI e il DI è lo schema di un localizzatore di servizio, perché il front-end deve fornire una chiave (presente nel registro del localizzatore di servizio) per consentire la risoluzione della sua richiesta. Esempio di localizzatore di servizi:

I_Dosomething x = ServiceLocator.returnDoing(String pKey);

DI esempio:

I_Dosomething x = DIContainer.returnThat();

Uno dei requisiti di DI è che il contenitore deve essere in grado di scoprire quale classe è l'implementazione di quale interfaccia. Quindi un contenitore DI richiede un design fortemente tipizzato e solo un'implementazione per ogni interfaccia allo stesso tempo. Se hai bisogno di più implementazioni di un'interfaccia allo stesso tempo (come una calcolatrice), hai bisogno del localizzatore di servizio o del modello di progettazione di fabbrica.

D (b) I: Iniezione delle dipendenze e progettazione per interfaccia. Questa restrizione non è un problema pratico molto grande. Il vantaggio dell'uso di D (b) I è che serve la comunicazione tra il cliente e il fornitore. Un'interfaccia è una prospettiva su un oggetto o un insieme di comportamenti. Quest'ultimo è cruciale qui.

Preferisco l'amministrazione dei contratti di servizio insieme a D (b) I nella codifica. Dovrebbero andare insieme. L'uso di D (b) I come soluzione tecnica senza amministrazione organizzativa dei contratti di servizio non è molto vantaggioso dal mio punto di vista, perché DI è quindi solo un ulteriore livello di incapsulamento. Ma quando puoi usarlo insieme all'amministrazione organizzativa puoi davvero utilizzare il principio organizzativo D (b) I. A lungo termine, può aiutarti a strutturare la comunicazione con il cliente e altri dipartimenti tecnici su argomenti come test, versioning e sviluppo di alternative. Quando si dispone di un'interfaccia implicita come in una classe con hardcoded, quindi è molto meno comunicabile nel tempo, quindi quando lo si rende esplicito utilizzando D (b) I. Tutto si riduce alla manutenzione, che è nel tempo e non alla volta. :-)