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

23

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

Quello che sto pensando è che posso creare alcune fabbriche specifiche che conterranno la logica della creazione di oggetti per alcuni specifici tipi di classe. 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 altri principi?

    
posta user3223738 27.06.2016 - 08:52
fonte

7 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: composizione root. Assicurarsi che l'intera configurazione sia eseguita sul punto di ingresso del progetto. Il resto del codice dovrebbe essere informato sulle iniezioni, non su qualsiasi strumento che viene utilizzato.

Terzo: la Iniezione del Costruttore è la via più probabile per andare (ci sono casi in cui non si vorrebbe, ma non così tanti).

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

    
risposta data 27.06.2016 - 09:26
fonte
13

Ci sono due parti alla tua domanda: come implementare correttamente DI e come rifattorizzare una grande applicazione per usare DI.

La prima parte riceve una risposta soddisfacente da @Miyamoto Akira (in particolare la raccomandazione di leggere il libro "dipendenza dalla dipendenza in .net" di Mark Seemann. Marks blog è anche una buona risorsa gratuita.

La seconda parte è molto più complicata.

Un buon primo passo sarebbe semplicemente spostare tutta l'istanziazione nei costruttori delle classi - non iniettare le dipendenze, assicurandoti solo di chiamare new nel costruttore.

Questo evidenzierà tutte le violazioni SRP che hai fatto, quindi puoi iniziare a suddividere la classe in collaboratori più piccoli.

Il prossimo problema che troverai saranno le classi che si basano sui parametri di runtime per la costruzione. Solitamente puoi risolvere questo problema creando semplici factory, spesso con Func<param,type> , inizializzandole nel costruttore e chiamandole nei metodi.

Il passo successivo sarebbe creare interfacce per le dipendenze e aggiungere un secondo costruttore alle classi che eccetto queste interfacce. Il tuo costruttore senza parametri aggiornerebbe 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 qualche test unitario, e se quello era l'obiettivo principale del refactoring, potrebbe essere il punto in cui ti fermi. Il nuovo codice sarà scritto con l'iniezione del costruttore, ma il tuo vecchio codice può continuare a funzionare come scritto, ma può essere testabile.

Ovviamente puoi andare oltre. Se si intende utilizzare un contenitore IOC, un passo successivo potrebbe essere quello di sostituire tutte le chiamate dirette a new nei costruttori senza parametri con chiamate statiche al contenitore IOC, essenzialmente (ab) utilizzandolo come servizio di localizzazione.

Questo 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 rifattorici su puro DI.

In fin dei conti questo sarà molto lavoro, quindi assicurati di decidere perché lo vuoi fare e prioritze le parti della base di codice che trarranno maggior beneficio dal refactore

    
risposta data 27.06.2016 - 12:03
fonte
1

Per prima cosa voglio menzionare che stai rendendo questo molto più difficile con te stesso rifattorizzando un progetto esistente piuttosto che avviando un nuovo progetto.

Hai detto che è una grande applicazione, quindi scegli un piccolo componente con cui iniziare. Preferibilmente un componente 'nodo foglia' che non viene usato da nient'altro. Non so quale sia lo stato dei test automatici su questa applicazione, ma interromperete tutti i test unitari per questo componente. Quindi preparati per quello. Il passaggio 0 sta scrivendo i test di integrazione per il componente che si modificherà se non esistono già. Come ultima risorsa (nessuna infrastruttura di test, nessun buy-in per scriverlo), immagina 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 è quello di rimuovere tutte le istanze dell'operatore 'nuovo' da questo componente. Questi generalmente rientrano in due categorie:

  1. Variabile membro invariante: queste sono 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. Generalmente non sei responsabile per lo smaltimento di questi oggetti (non voglio dire mai qui, ma in realtà non dovresti avere questa responsabilità).

  2. Variabile membro variabile / variabile metodo: queste sono le variabili che otterranno la garbage collection in qualche momento durante la vita dell'oggetto. Per questi, ti consigliamo di iniettare una fabbrica nella tua classe per fornire queste istanze. Sei responsabile per lo smaltimento di oggetti creati da una fabbrica.

Il tuo contenitore IoC (come suona come) si prenderà la responsabilità di istanziare quegli oggetti e implementare le tue interfacce di fabbrica. Qualunque cosa stia utilizzando il componente che hai modificato dovrà conoscere il contenitore IoC in modo che possa recuperare il tuo componente.

Una volta completato quanto sopra, sarai in grado di raccogliere tutti i benefici che speri di ottenere da DI nel componente selezionato. Ora sarebbe un buon momento per aggiungere / correggere quei test unitari. Se esistessero test unitari, dovresti prendere una decisione sul fatto che vuoi ricollegarli iniettando oggetti reali o scrivendo nuovi test unitari usando i mock.

'Semplicemente' ripeti il precedente per ogni componente della tua applicazione, spostando il riferimento al contenitore IoC fino a quando non ne hai solo bisogno.

    
risposta data 27.06.2016 - 16:16
fonte
0

L'approccio corretto è usare l'iniezione del costruttore, se usi

What I'm thinking about is that I can create a few specific factories which will contain logic of creating objects for a few specific class types. Basically a static class with a method invoking Ninject Get() method of a static kernel instance in this class.

quindi si finisce con il localizzatore di servizio, rispetto all'iniezione di dipendenza.

    
risposta data 27.06.2016 - 09:00
fonte
0

Dici che vuoi usarlo ma non dire perché.

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

Questo di per sé deriva dal DIP . Se il tuo codice è già scritto in questo stile e hai un singolo luogo in cui vengono generate le concrezioni, DI non porta più nulla alla festa. Aggiungendo il codice quadro di DI qui, si limiterebbe a gonfiare e offuscare la base di codice.

Supponendo che do voglia utilizzarlo, in genere si imposta factory / builder / container (o qualsiasi altra cosa) all'inizio dell'applicazione in modo che sia chiaramente visibile.

NB. è molto facile eseguire il rollover se si desidera piuttosto che eseguire il commit su Ninject / StructureMap o altro. Se tuttavia si ha un ragionevole ricambio di personale, può ungere le ruote per utilizzare un framework riconosciuto o almeno scriverlo in quello stile in modo che non sia una curva di apprendimento eccessiva.

    
risposta data 27.06.2016 - 12:11
fonte
0

In realtà, il modo "giusto" è NON usare affatto una fabbrica a meno che non ci sia assolutamente altra scelta (come nei test unitari e certi mock - per il codice di produzione NON si usa una fabbrica)! Fare così è in realtà un anti-modello e dovrebbe essere evitato a tutti i costi. L'intero punto dietro un contenitore DI è consentire al gadget di eseguire il lavoro per .

Come indicato sopra in un post precedente, desideri che il tuo gadget IoC si assuma la responsabilità della creazione dei vari oggetti dipendenti nella tua app. Ciò significa lasciare che il tuo gadget DI crei e gestisca le varie istanze stesse. Questo è il punto dietro DI - i tuoi oggetti non dovrebbero MAI sapere come creare e / o gestire gli oggetti da cui dipendono. Fare altrimenti pause accoppiamento lento.

La conversione di un'applicazione esistente in tutti i DI è un grande passo, ma mettendo da parte le ovvie difficoltà nel fare ciò, vorrai anche (solo per semplificarti la vita) per esplorare uno strumento DI che eseguirà il grosso di i tuoi binding automaticamente (il nucleo di qualcosa come Ninject sono le chiamate "kernel.Bind<someInterface>().To<someConcreteClass>()" che fai per abbinare le dichiarazioni dell'interfaccia a quelle classi concrete che desideri utilizzare per implementare tali interfacce. Sono quelle chiamate "Bind" che consentono al tuo gadget DI di intercettare il costruttore chiama e fornisce le istanze dell'oggetto dipendente necessarie. Un tipico costruttore (pseudo codice 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);
  }
}

Nota che da nessuna parte in quel codice era qualsiasi codice che creava / gestiva / rilasciava l'istanza di SomeConcreteClassA o SomeOtherConcreteClassB. Di fatto, nessuna classe concreta è stata nemmeno citata. Quindi ... dove è avvenuta la magia?

Nella parte di avvio della tua app, ha avuto luogo (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>();
}

Un po 'di codice ci dice che il gadget di Ninject cerca i costruttori, li scannerizza, cerca le istanze di interfacce che è stata configurata per gestire (cioè le chiamate "Bind") e quindi crea e sostituisce un'istanza del classe concreta ovunque venga fatta riferimento all'istanza.

C'è un bel tool che integra Ninject molto bene chiamato Ninject.Extensions.Conventions (ancora un altro pacchetto NuGet) che farà la maggior parte di questo lavoro per te. Per non tralasciare l'eccellente esperienza di apprendimento che dovrai affrontare mentre sviluppi questo, ma per iniziare, questo potrebbe essere uno strumento da investigare.

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 assistenti 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 commettere errori evidenti - Mark Ho dimenticato tutto di Ninject nel suo libro, quindi ecco un'altra risorsa scritta proprio per Ninject. Ce l'ho ed è una buona lettura: Padroneggiare Ninject for Dependency Injection

    
risposta data 27.06.2016 - 20:34
fonte
0

Non esiste una "strada giusta", ma ci sono alcuni semplici principi da seguire:

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

Questo è tutto. Di sicuro, questi sono principi non leggi, ma se li segui puoi essere certo di fare DI (correggimi se ho torto).

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

Nel caso di NInject, esiste una estensione di fabbrica che fornisce la creazione di fabbriche. Certo, le fabbriche create hanno ancora una referenza interna al kernel, ma non è accessibile dalla tua applicazione.

    
risposta data 27.06.2016 - 20:10
fonte