Design – Dove devono essere registrati gli oggetti quando si usa Windsor

Avrò i seguenti componenti nella mia applicazione

  • Accesso ai dati
  • DataAccess.Test
  • Attività commerciale
  • Business.Test
  • Applicazione

Speravo di usare Castle Windsor come IoC per incollare gli strati insieme, ma sono un po ‘incerto sul design dell’incollaggio.

La mia domanda è: chi dovrebbe essere responsabile della registrazione degli oggetti in Windsor? Ho un paio di idee;

  1. Ogni livello può registrare i propri oggetti. Per testare il BL, il banco di prova potrebbe registrare classi di simulazione per il DAL.
  2. Ogni livello può registrare l’object delle sue dipendenze, ad esempio il livello aziendale registra i componenti del livello di accesso ai dati. Per testare il BL, il banco di prova dovrebbe scaricare l’object DAL “reale” e registrare gli oggetti finti.
  3. L’applicazione (o l’app di test) registra tutti gli oggetti delle dipendenze.

Qualcuno può aiutarmi con alcune idee e pro / contro con i diversi percorsi? I collegamenti ai progetti di esempio che utilizzano Castle Windsor in questo modo sarebbero molto utili.

In generale, tutti i componenti di un’applicazione devono essere composti il ​​più tardi ansible, perché ciò garantisce la massima modularità e che i moduli sono il meno ansible accoppiati.

In pratica, ciò significa che è necessario configurare il contenitore nella radice dell’applicazione.

  • In un’app desktop, questo sarebbe nel metodo Main (o molto vicino ad esso)
  • In un’applicazione ASP.NET (incluso MVC), questo sarebbe in Global.asax
  • In WCF, sarebbe in un ServiceHostFactory
  • eccetera.

Il contenitore è semplicemente il motore che compone i moduli in un’applicazione funzionante. In linea di principio, potresti scrivere il codice a mano (questo è chiamato DI di Poor Man ), ma è molto più facile usare un contenitore DI come Windsor.

Tale Root di composizione sarà idealmente l’unica parte di codice nella root dell’applicazione, rendendo l’applicazione un cosiddetto Humble Executable (un termine dagli eccellenti xUnit Test Pattern ) che non ha bisogno di test delle unità in sé.

I tuoi test non dovrebbero avere bisogno del contenitore, in quanto i tuoi oggetti e moduli dovrebbero essere componibili, e puoi direttamente fornire Test Doubles ai test unitari. È meglio se puoi progettare tutti i tuoi moduli come indipendenti dal contenitore.

Inoltre, in particolare in Windsor, dovresti incapsulare la logica di registrazione dei componenti all’interno degli installer (tipi che implementano IWindsorInstaller ) Vedi la documentazione per maggiori dettagli

Mentre la risposta di Mark è ottima per gli scenari web, il difetto chiave con l’applicazione per tutte le architetture (vale a dire rich-client – cioè: WPF, WinForms, iOS, ecc.) È l’ipotesi che tutti i componenti necessari per un’operazione possano / dovrebbero essere creati subito.

Per i server Web questo ha senso dal momento che ogni richiesta ha una vita estremamente breve e un controller MVC ASP.NET viene creato dal framework sottostante (nessun codice utente) per ogni richiesta. Quindi il controller e tutte le sue dipendenze possono essere facilmente composti da un framework DI, e ci sono pochissimi costi di manutenzione per farlo. Si noti che il framework web è responsabile della gestione della vita del controller e per tutti gli scopi della durata di tutte le sue dipendenze (che il framework DI creerà / inietterà per voi al momento della creazione del controller). È del tutto normale che le dipendenze vadano per la durata della richiesta e che il codice utente non debba gestire la durata di componenti e sottocomponenti stessi. Si noti inoltre che i server Web sono stateless su richieste diverse (ad eccezione dello stato della sessione, ma non è rilevante per questa discussione) e che non si hanno più istanze controller / controller figlio che devono vivere contemporaneamente per soddisfare una singola richiesta.

Nelle app rich-client tuttavia questo non è il caso. Se si utilizza un’architettura MVC / MVVM (cosa che si dovrebbe!) La sessione di un utente è longeva e i controllori creano controller secondari / controllanti di pari livello mentre l’utente naviga attraverso l’app (vedere la nota su MVVM in basso). L’analogia con il mondo web è che ogni input dell’utente (clic del pulsante, operazione eseguita) in un’app rich-client è l’equivalente di una richiesta ricevuta dal framework web. La grande differenza tuttavia è che si desidera che i controller di un’app rich client rimangano in vita tra le operazioni (molto probabile che l’utente esegua più operazioni sullo stesso schermo, che è governato da un particolare controller) e anche che i subcontroller ottengano creato e distrutto quando l’utente esegue azioni diverse (pensa a un controllo a tabs che crea pigramente la scheda se l’utente naviga verso di esso, o una parte dell’interfaccia utente che deve essere caricata solo se l’utente esegue determinate azioni su uno schermo).

Entrambe queste caratteristiche significano che è il codice utente che deve gestire la vita dei controller / sub-controller e che le dipendenze dei controller NON devono essere tutte create in anticipo (es .: sottocontrolli, modelli di visualizzazione, altri componenti di presentazione ecc. ). Se si utilizza un framework DI per eseguire queste responsabilità, si otterrà non solo un numero molto maggiore di codice a cui non appartiene (vedere: Anti-pattern di over-injection del costruttore ) ma sarà anche necessario passare lungo un contenitore di dipendenza in tutto la maggior parte del livello di presentazione in modo che i componenti possano utilizzarlo per creare i relativi componenti secondari quando necessario.

Perché è sbagliato che il mio codice utente abbia accesso al contenitore DI?

1) Il contenitore delle dipendenze contiene riferimenti a molti componenti nella tua app. Passare questo cattivo ragazzo a ogni componente che ha bisogno di creare / gestire un sottocomponente anoterico è l’equivalente dell’uso di globals nella tua architettura. Ancora peggio di qualsiasi sottocomponente è ansible registrare nuovi componenti nel contenitore così presto diventerà anche uno storage globale. Gli sviluppatori getteranno oggetti nel contenitore solo per passare i dati tra i componenti (tra i controllori di pari livello o tra le gerarchie di deep controller), ovvero: un controllore di antenati deve prelevare i dati da un controller di nonno). Nota che nel mondo web in cui il contenitore non viene passato al codice utente questo non è mai un problema.

2) L’altro problema con i contenitori di dipendenza rispetto ai locatori di servizio / alle fabbriche / all’istanziazione diretta degli oggetti è che la risoluzione da un contenitore rende completamente ambiguo se si sta creando un componente o semplicemente RIUSANDO uno esistente. Invece è lasciato a una configurazione centralizzata (es .: bootstrapper / Composition Root) per capire quale sia la durata del componente. In alcuni casi va bene (es: web controller, dove non è il codice utente che deve gestire la vita del componente ma lo stesso framework di elaborazione delle richieste di runtime). Ciò è estremamente problematico, tuttavia, quando il design dei componenti deve INDICARE se è responsabilità di gestire un componente e quale dovrebbe essere la sua durata (Esempio: un’app del telefono fa apparire un foglio che richiede all’utente alcune informazioni. controller che crea un sottocontrollo che governa il foglio di sovrapposizione: quando l’utente inserisce alcune informazioni, il foglio viene dimesso e il controllo viene restituito al controller iniziale, che mantiene ancora lo stato da ciò che l’utente stava facendo in precedenza. Se si usa DI per risolvere il sottocontrollo del foglio, è ambiguo quale dovrebbe essere la sua durata o chi dovrebbe essere responsabile della sua gestione (il controllore iniziale). Confronta questo con la responsabilità esplicita dettata dall’uso di altri meccanismi.

Scenario A:

 // not sure whether I'm responsible for creating the thing or not DependencyContainer.GimmeA() 

Scenario B:

 // responsibility is clear that this component is responsible for creation Factory.CreateMeA() // or simply new Thing() 

Scenario C:

 // responsibility is clear that this component is not responsible for creation, but rather only consumption ServiceLocator.GetMeTheExisting() // or simply ServiceLocator.Thing 

Come potete vedere, DI rende poco chiaro chi è responsabile della gestione a vita del sottocomponente.

NOTA: Tecnicamente parlando, molti framework DI hanno un modo di creare componenti pigramente (Vedi: Come non fare l’iniezione di dipendenza – il container statico o singleton ) che è molto meglio che passare il container in giro, ma si sta ancora pagando il costo di mutazione del codice per passare dappertutto alle funzioni di creazione, manca il supporto di primo livello per il passaggio dei parametri validi del costruttore durante la creazione e alla fine del giorno si utilizza ancora un meccanismo indiretto in luoghi in cui l’unico vantaggio è ottenere la testabilità , che può essere raggiunto in modi migliori e più semplici (vedi sotto).

Che cosa significa tutto questo?

Significa che DI è appropriato per determinati scenari e inappropriato per gli altri. Nelle applicazioni rich-client capita di portare molti degli aspetti negativi di DI con pochissimi dei lati positivi. Più la tua app si ridimensiona in complessità, maggiori saranno i costi di manutenzione. Trasporta anche il grave potenziale di uso improprio, che a seconda di quanto siano stretti i processi di comunicazione e di revisione del codice del team, può essere ovunque, da un non-problema a un grave costo del debito tecnologico. C’è un mito che circola sul fatto che i Service Locator o le Fabbriche o il buon vecchio Instantiation sono meccanismi in qualche modo scadenti e semplicemente obsoleti semplicemente perché potrebbero non essere il meccanismo ottimale nel mondo delle app web, dove forse un sacco di persone giocano. generalizzare questi apprendimenti a tutti gli scenari e visualizzare tutto come chiodi solo perché abbiamo imparato a maneggiare un particolare martello.

La mia raccomandazione PER APPL-CLIENT APPS è quella di utilizzare il meccanismo minimo che soddisfa i requisiti per ciascun componente a portata di mano. L’80% delle volte dovrebbe essere una istanziazione diretta. I locatori di servizi possono essere utilizzati per ospitare i principali componenti del livello aziendale (es .: servizi applicativi generalmente di natura singleton) e naturalmente anche le fabbriche e persino il modello Singleton. Non c’è niente da dire che non puoi usare un framework DI nascosto dietro il tuo localizzatore di servizi per creare le dipendenze del tuo livello aziendale e tutto ciò su cui dipendono in un colpo solo – se questo finisce per semplificarti la vita in quel livello, e quel livello non lo fa t esibiscono il carico pigro che gli strati di presentazione rich-client fanno in modo schiacciante . Assicurati solo di proteggere il tuo codice utente dall’accesso a quel contenitore in modo da poter prevenire il disordine che può creare un contenitore DI in giro.

Che dire della testabilità?

La testabilità può essere assolutamente raggiunta senza un framework DI. Raccomando l’uso di un framework di intercettazione come UnitBox (gratuito) o TypeMock (costoso). Questi framework ti forniscono gli strumenti necessari per aggirare il problema (come prendi in giro l’istanziazione e le chiamate statiche in C #) e non ti richiedono di cambiare l’intera architettura per aggirarli (che purtroppo è dove si trova la tendenza andato nel mondo. NET / Java). È più saggio trovare una soluzione al problema in questione e utilizzare i meccanismi del linguaggio naturale e i modelli ottimali per il componente sottostante, quindi provare a inserire tutti i pioli quadrati nel foro DI rotondo. Una volta che inizierai a utilizzare questi meccanismi più semplici e specifici, noterai che c’è poco bisogno di DI nella tua base di codici, se non del tutto.

NOTA: per le architetture MVVM

Nelle architetture MVVM di base, i modelli di visualizzazione assumono effettivamente la responsabilità dei responsabili del trattamento, pertanto per tutti gli aspetti considerare la dicitura “controller” sopra riportata per applicare a “view-model”. MVVM di base funziona bene per le piccole app, ma con la crescita della complessità di un’applicazione potresti voler utilizzare un approccio MVCVM. I modelli di visualizzazione diventano per lo più stupidi DTO per facilitare l’associazione dei dati alla vista mentre l’interazione con il livello aziendale e tra i gruppi di modelli di visualizzazione che rappresentano schermate / sottoschermate vengono incapsulati in componenti espliciti di controller / sottocontrollo. In entrambe le architetture esiste la responsabilità dei controllori e presenta le stesse caratteristiche sopra discusse.