Qual è il modo "giusto" per implementare DI in .NET?


22

Sto cercando di implementare l'iniezione di dipendenza in un'applicazione relativamente grande ma non ho esperienza in essa. Ho studiato il concetto e alcune implementazioni di IoC e iniettori di dipendenza disponibili, come Unity e Ninject. Tuttavia, c'è una cosa che mi sfugge. Come devo organizzare la creazione dell'istanza nella mia applicazione?

Quello a cui sto pensando è che posso creare alcune fabbriche specifiche che conterranno la logica di creazione di oggetti per alcuni tipi di classe specifici. Fondamentalmente una classe statica con un metodo che richiama il metodo Ninject Get () di un'istanza del kernel statico in questa classe.

Sarà un approccio corretto per implementare l'iniezione di dipendenza nella mia applicazione o dovrei implementarla secondo qualche altro principio?


5
Non penso che ci sia la strada giusta, ma molti modi giusti, a seconda del tuo progetto. Aderirei agli altri e suggerirei l'iniezione del costruttore, poiché sarai in grado di assicurarti che ogni dipendenza venga iniettata su un singolo punto. Inoltre, se le firme del costruttore crescono troppo a lungo, saprai che le classi fanno troppo.
Paul Kertscher,

È difficile rispondere senza sapere che tipo di progetto .net stai realizzando. Una buona risposta per WPF potrebbe essere una cattiva risposta per MVC, ad esempio.
JMK,

È bello organizzare tutte le registrazioni delle dipendenze in un modulo DI per la soluzione o per ciascun progetto, e possibilmente uno per alcuni test a seconda del profondo che si desidera testare. Oh sì, ovviamente dovresti usare l'iniezione del costruttore, l'altra roba è per usi più avanzati / folli.
Mark Rogers,

Risposte:


30

Non pensare ancora allo strumento che intendi utilizzare. Puoi fare DI senza un contenitore IoC.

Primo punto: Mark Seemann ha un ottimo libro su DI in .Net

Secondo: radice della composizione. Assicurarsi che l'intero set up sia eseguito nel punto di ingresso del progetto. Il resto del codice dovrebbe essere a conoscenza delle iniezioni, non di qualsiasi strumento utilizzato.

Terzo: l'iniezione del costruttore è il modo più probabile di procedere (ci sono casi in cui non lo vorresti, ma non così tanti).

Quarto: esaminare l'utilizzo delle fabbriche lambda e altre caratteristiche simili per evitare di creare interfacce / classi non necessarie al solo scopo di iniezione.


5
Tutti i consigli eccellenti; in particolare la prima parte: impara come fare DI puro, e poi inizia a guardare i contenitori IoC che possono ridurre la quantità di codice della piastra di caldaia richiesta da quell'approccio.
David Arno,

6
... o salta tutti i contenitori IoC insieme - per mantenere tutti i vantaggi della verifica statica.
Den,

Mi piace che questo consiglio sia un buon punto di partenza, prima di entrare in DI, e in realtà ho il libro Mark Seemann.
Snoop,

Posso assecondare questa risposta. Abbiamo usato con successo poor-mans-DI (bootstrapper scritto a mano) in un'applicazione abbastanza grande strappando parti della logica di bootstrapper nei moduli che componevano l'applicazione.
parrucca il

1
Stai attento. L'uso di iniezioni di lambda può essere un passo veloce verso la follia, in particolare del tipo di danno di progettazione indotto dal test. Lo so. Ho seguito quella strada.
jpmc26,

13

Ci sono due parti della tua domanda: come implementare correttamente DI e come refactoring di una grande applicazione per usare DI.

Alla prima parte risponde bene @Miyamoto Akira (in particolare la raccomandazione di leggere il libro "Iniezione delle dipendenze in .net" di Mark Seemann. Il blog di Marks è anche una buona risorsa gratuita.

La seconda parte è molto più complicata.

Un buon primo passo sarebbe semplicemente spostare tutta l'istanza nei costruttori di classi, non iniettando le dipendenze, ma solo assicurandoti di chiamare solo newil costruttore.

Ciò metterà in evidenza tutte le violazioni di SRP che hai commesso, in modo da poter iniziare a suddividere la classe in collaboratori più piccoli.

Il prossimo numero che troverai saranno le classi che si basano su parametri di runtime per la costruzione. Di solito è possibile risolvere questo problema creando semplici fabbriche, spesso con Func<param,type>, inizializzandole nel costruttore e chiamandole nei metodi.

Il prossimo passo sarebbe creare interfacce per le tue dipendenze e aggiungere un secondo costruttore alle tue classi che ad eccezione di queste interfacce. Il costruttore senza parametri ripristinerebbe le istanze concrete e le passerebbe al nuovo costruttore. Questo è comunemente chiamato 'B * stard Injection' o 'Poor mans DI'.

Questo ti darà la possibilità di fare alcuni test unitari e se quello era l'obiettivo principale del refattore, potrebbe essere dove ti fermi. Il nuovo codice verrà scritto con l'iniezione del costruttore, ma il tuo vecchio codice può continuare a funzionare come scritto ma è ancora testabile.

Ovviamente puoi andare oltre. Se si intende utilizzare un contenitore IOC, un passaggio successivo potrebbe essere quello di sostituire tutte le chiamate dirette newnei costruttori senza parametri con chiamate statiche al contenitore IOC, essenzialmente (ab) usandolo come localizzatore di servizi.

Ciò genererà più casi di parametri del costruttore di runtime da gestire come prima.

Una volta fatto ciò, puoi iniziare a rimuovere i costruttori senza parametri e fare il refactor su DI puro.

Alla fine questo richiederà molto lavoro, quindi assicurati di decidere perché vuoi farlo, e dai la priorità alle parti della base di codice che trarranno il massimo beneficio dal refactor


3
Grazie per una risposta molto elaborata. Mi hai dato alcune idee su come affrontare il problema che sto affrontando. L'intera architettura dell'app è già costruita pensando all'IoC. Il motivo principale per cui voglio usare DI non è nemmeno il test unitario, si presenta come un bonus ma piuttosto come una capacità di scambiare diverse implementazioni con interfacce diverse, definite nel cuore della mia applicazione, con il minor sforzo possibile. L'applicazione in questione funziona in un ambiente in costante cambiamento e spesso devo scambiare parti dell'applicazione per utilizzare nuove implementazioni in base ai cambiamenti nell'ambiente.
user3223738,

1
Sono contento di poter aiutare, e sì, sono d'accordo sul fatto che il più grande vantaggio di DI è l'accoppiamento libero e la possibilità di riconfigurare facilmente ciò che porta, con i test unitari sono un piacevole effetto collaterale.
Steve,

1

Prima di tutto voglio menzionare che stai rendendo questo fatto molto più difficile per te stesso riformattando un progetto esistente piuttosto che avviarne uno nuovo.

Hai detto che si tratta di un'applicazione di grandi dimensioni, quindi scegli un piccolo componente per iniziare. Preferibilmente un componente 'leaf-node' che non viene utilizzato da nient'altro. Non so quale sia lo stato dei test automatizzati su questa applicazione, ma supererai tutti i test unitari per questo componente. Quindi preparati per questo. Il passaggio 0 sta scrivendo i test di integrazione per il componente che modificherai se non esistono già. Come ultima risorsa (nessuna infrastruttura di test; nessun buy-in per scriverlo), capire una serie di test manuali che puoi fare per verificare che questo componente funzioni.

Il modo più semplice per dichiarare il tuo obiettivo per il refactor DI è che vuoi rimuovere tutte le istanze del "nuovo" operatore da questo componente. Questi generalmente rientrano in due categorie:

  1. Variabile membro invariante: si tratta di variabili che vengono impostate una volta (in genere nel costruttore) e non vengono riassegnate per la durata dell'oggetto. Per questi è possibile iniettare un'istanza dell'oggetto nel costruttore. In genere non sei responsabile dello smaltimento di questi oggetti (non voglio dirlo mai qui, ma non dovresti avere questa responsabilità).

  2. Variabile membro variabile / variabile metodo: si tratta di variabili che verranno raccolte durante un ciclo di vita dell'oggetto durante la vita dell'oggetto. Per questi, ti consigliamo di iniettare una factory nella tua classe per fornire queste istanze. Sei responsabile dello smaltimento degli oggetti creati da una fabbrica.

Il tuo contenitore IoC (a quanto pare) si assumerà la responsabilità di istanziare quegli oggetti e implementare le interfacce di fabbrica. Qualunque cosa stia usando il componente che hai modificato, dovrai conoscere il contenitore IoC in modo che possa recuperare il tuo componente.

Una volta completato quanto sopra, sarai in grado di raccogliere tutti i vantaggi che speri di ottenere da DI nel componente selezionato. Ora sarebbe un buon momento per aggiungere / correggere quei test unitari. Se esistevano test unitari esistenti, dovrai decidere se collegarli insieme iniettando oggetti reali o scrivere nuovi test unitari usando simulazioni.

'Semplicemente' ripeti quanto sopra per ciascun componente dell'applicazione, spostando il riferimento al contenitore IoC mentre procedi fino a quando solo il principale deve saperlo.


1
Un buon consiglio: inizia con un piccolo componente piuttosto che con la "GRANDE riscrittura"
Daniel Hollinrake,

0

L'approccio corretto consiste nell'utilizzare l'iniezione del costruttore, se si utilizza

Quello a cui sto pensando è che posso creare alcune fabbriche specifiche che conterranno la logica di creazione di oggetti per alcuni tipi di classe specifici. Fondamentalmente una classe statica con un metodo che richiama il metodo Ninject Get () di un'istanza del kernel statico in questa classe.

poi si finisce con il localizzatore di servizio, che l'iniezione di dipendenza.


Sicuro. Iniezione del costruttore. Diciamo che ho una classe che accetta un'implementazione dell'interfaccia come uno degli argomenti. Ma devo ancora creare un'istanza dell'implementazione dell'interfaccia e passarla a questo costruttore da qualche parte. Preferibilmente dovrebbe essere un pezzo di codice centralizzato.
user3223738

2
No, quando si inizializza il contenitore DI, è necessario specificare l'implementazione delle interfacce e il contenitore DI creerà l'istanza e inietterà nel costruttore.
Low Flying Pelican il

Personalmente, trovo che l'iniezione del costruttore sia abusata. Ho visto troppo spesso che vengono iniettati 10 diversi servizi e ne è effettivamente necessario uno solo per una chiamata di funzione - perché allora quella parte dell'argomento della funzione non è?
urbanhusky,

2
Se vengono iniettati 10 servizi diversi, è perché qualcuno sta violando SRP, che dovrebbe essere suddiviso in componenti più piccoli.
Low Flying Pelican il

1
@Fabio La domanda è cosa ti comprerebbe. Devo ancora vedere un esempio in cui avere una classe gigante che si occupa di una dozzina di cose totalmente diverse è un buon design. L'unica cosa che DI fa è rendere più evidenti tutte quelle violazioni.
Voo,

0

Dici di volerlo usare ma non dichiari il perché.

DI non è altro che fornire un meccanismo per generare concrezioni dalle interfacce.

Questo di per sé viene dal DIP . Se il tuo codice è già scritto in questo stile e hai un unico posto in cui vengono generate concrezioni, DI non porta altro alla festa. L'aggiunta di un codice framework DI qui potrebbe semplicemente gonfiare e offuscare la tua base di codice.

Dando per scontato che non desidera utilizzare, in genere impostare la fabbrica / costruttore / contenitore (o altro) nelle prime fasi del applicazione in modo che sia chiaramente visibile.

NB è molto semplice realizzare il proprio, se lo si desidera, piuttosto che impegnarsi in Ninject / StructureMap o altro. Se tuttavia si dispone di un ragionevole turnover del personale, è possibile ingrassare le ruote per utilizzare un framework riconosciuto o almeno scriverlo in quello stile in modo che non sia troppo una curva di apprendimento.


0

In realtà, il modo "giusto" è di NON usare affatto una fabbrica a meno che non ci sia assolutamente altra scelta (come nei test unitari e in alcuni derisione - per il codice di produzione NON si utilizza una fabbrica)! Farlo è in realtà un anti-schema e dovrebbe essere evitato a tutti i costi. L'intero punto dietro un contenitore DI è consentire al gadget di fare il lavoro per te.

Come indicato in precedenza in un post precedente, vuoi che il tuo gadget IoC si assuma la responsabilità della creazione dei vari oggetti dipendenti nella tua app. Ciò significa consentire al tuo gadget DI di creare e gestire le varie istanze stesse. Questo è il punto alla base di DI: i tuoi oggetti non dovrebbero MAI sapere come creare e / o gestire gli oggetti da cui dipendono. In caso contrario si rompe l' accoppiamento lento.

La conversione di un'applicazione esistente in tutti i DI è un grande passo, ma mettendo da parte le ovvie difficoltà nel farlo, vorrai anche (solo per semplificarti un po 'la vita) esplorare uno strumento DI che eseguirà automaticamente la maggior parte dei tuoi binding (il nucleo di qualcosa come Ninject sono le "kernel.Bind<someInterface>().To<someConcreteClass>()"chiamate che fai per abbinare le dichiarazioni della tua interfaccia a quelle classi concrete che desideri utilizzare per implementare quelle interfacce. Sono quelle chiamate "Bind" che consentono al tuo gadget DI di intercettare le tue chiamate del costruttore e fornire il istanze di oggetti dipendenti necessarie Un tipico costruttore (codice pseudo mostrato qui) per alcune classi potrebbe essere:

public class SomeClass
{
  private ISomeClassA _ClassA;
  private ISomeOtherClassB _ClassB;

  public SomeClass(ISomeClassA aInstanceOfA, ISomeOtherClassB aInstanceOfB)
  {
    if (aInstanceOfA == null)
      throw new NullArgumentException();
    if (aInstanceOfB == null)
      throw new NullArgumentException();
    _ClassA = aInstanceOfA;
    _ClassB = aInstanceOfB;
  }

  public void DoSomething()
  {
    _ClassA.PerformSomeAction();
    _ClassB.PerformSomeOtherActionUsingTheInstanceOfClassA(_ClassA);
  }
}

Si noti che nessuna parte in quel codice era alcun codice che ha creato / gestito / rilasciato sia l'istanza di SomeConcreteClassA o SomeOtherConcreteClassB. È un dato di fatto, né a nessuna classe concreta è stato neppure fatto riferimento. Quindi ... dove è avvenuta la magia?

Nella parte di avvio della tua app, si è verificato quanto segue (di nuovo, questo è pseudo codice ma è abbastanza vicino alla cosa reale (Ninject) ...):

public void StartUp()
{
  kernel.Bind<ISomeClassA>().To<SomeConcreteClassA>();
  kernel.Bind<ISomeOtherClassB>().To<SomeOtherConcreteClassB>();
}

Quel po 'di codice lì dice al gadget Ninject di cercare costruttori, scansionarli, cercare istanze di interfacce che è stato configurato per gestire (cioè le chiamate "Bind") e quindi creare e sostituire un'istanza della classe concreta ovunque si fa riferimento all'istanza.

Esiste un ottimo strumento che completa Ninject molto bene chiamato Ninject.Extensions.Conventions (l'ennesimo pacchetto NuGet) che farà la maggior parte di questo lavoro per te. Non per toglierti l'eccellente esperienza di apprendimento che attraverserai mentre lo costruisci da solo, ma per iniziare, questo potrebbe essere uno strumento per indagare.

Se la memoria serve, Unity (formalmente da Microsoft ora un progetto Open Source) ha una chiamata al metodo o due che fanno la stessa cosa, altri strumenti hanno aiutanti simili.

Qualunque sia il percorso che scegli, leggi sicuramente il libro di Mark Seemann per la maggior parte della tua formazione DI, tuttavia, va sottolineato che anche i "Grandi" del mondo dell'ingegneria del software (come Mark) possono fare errori evidenti - Mark ha dimenticato tutto Ninject nel suo libro, quindi ecco un'altra risorsa scritta solo per Ninject. Ce l' ho ed è una buona lettura: Mastering Ninject per Dependency Injection


0

Non esiste un "modo giusto", ma ci sono alcuni semplici principi da seguire:

  • Crea la radice della composizione all'avvio dell'applicazione
  • Dopo aver creato la root di composizione, getta via il riferimento al contenitore / kernel DI (o almeno incapsulalo in modo che non sia direttamente accessibile dalla tua applicazione)
  • Non creare istanze tramite "nuovo"
  • Passare tutte le dipendenze richieste come astrazione al costruttore

È tutto. Sicuramente quelli sono principi non leggi, ma se li segui puoi essere sicuro di fare DI (per favore correggimi se sbaglio).


Quindi, come creare oggetti durante il runtime senza "nuovo" e senza conoscere il contenitore DI?

Nel caso di NInject, esiste un'estensione di fabbrica che fornisce la creazione di fabbriche. Certo, le fabbriche create hanno ancora un riferimento interno al kernel, ma questo non è accessibile dalla tua applicazione.

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.