Di tutti i concetti che compongono Prism, il concetto di “Container” è sicuramente il più difficile da affrontare concettualmente. In pratica, in un ambiente modulare, uno degli obiettivi deve essere rappresentato dal disaccoppiamento spinto dei moduli. Questo obiettivo, però, non deve far dimenticare che questi stessi moduli devono poter comunicare tra loro e con la Shell che li ospita.
Per raggiungere questo obiettivo, l'infrastruttura di CAL mette a disposizione un dependency injection container. Un container consente effettivamente di ridurre l’accoppiamento tra i vari componenti, permettendo la creazione, la gestione ed il ciclo di vita delle entità basandosi sulla configurazione del container stesso.
Per fissare le idee, mi piace associare il Container al vecchio fustino di detersivo pieno di pezzi Lego, con i quali giocavo quando ero bambino. Quando mi serviva un particolare pezzo (una base da 4, oppure un assale da 12 o una cremagliera) ribaltavo l’intero fustino per trovare il pezzo in mezzo a tutti gli altri. Il container di CAL non fa ne più nè meno che la stessa cosa: quando un oggetto deve essere creato, il container inietta nella costruttore gli oggetti del tipo descritto dalle interfacce presenti nella firma del costruttore dell’oggetto stesso. E’ possibile dire quindi che la base da 4, l’assale o la cremagliera sono Interfacce e che quando si crea un componente che necessita di questi elementi, nel suo costruttore verranno “iniettate” la base da 4, la cremagliera e l’assale (questa volta come oggetti) presenti nel fustino (container).
I vantaggi derivanti dall’uso di un container sono:
- non è più necessario gestire da un componente le sue dipendenze o il loro ciclo di vita;
- è facile scambiare le implementazioni concrete delle interfacce utilizzate dai componenti;
- è molto più agevole testare le funzionalità della nostra applicazione;
- è agevole far evolvere il sistema, permettendo di aggiungere funzionalità in tempi successivi;
Nel caso di Prism, l’utilizzo di Unity Application Block come Dependency Injection Container, fa emergere i seguenti vantaggi:
- le dipendenze vengono automaticamente iniettate all’interno del modulo quando quest’ultimo viene caricato;
- i ViewModels e le Views vengono registati e risolti dal container;
- i servizi della nostra applicazione possono essere immagazzinati nel container ed utilizzati dai moduli che necessitano del particolare servizio;
Un esempio di utilizzo di container si può vedere nel codice seguente, corpo di inizializzazione di un modulo:
1: //Costruttore
2: public MyModule(IUnityContainer container, IRegionManager regionManager)
3: {
4: _container = container;
5: _regionManagerService = regionManager;
6: }
7:
8: //Implementazione Interfaccia IModule
9: public void Initialize()
10: {
11: RegisterViewsAndServices();
12: //snip...
13: }
14:
15: //Registrazione oggetti all'interno del container
16: protected void RegisterViewsAndServices()
17: {
18: _container.RegisterType<IAccountPositionService, AccountPositionService>(new ContainerControlledLifetimeManager());
19: _container.RegisterType<ISummaryView, SummaryView>();
20: _container.RegisterType<ISummaryPresentationModel, SummaryPresentationModel>();
21: //snip...
22: }
Utilizzare il Container: Registering & Resolving
Per utilizzare un oggetto reperendolo dal container, quest’oggetto deve prima essere registrato all’interno del container. Questo può essere fatto o specificando un oggetto concreto, oppure un’interfaccia, in modo da lasciare al container l’onere di creare l’oggetto la prima volta che verrà “risolto”. Gli oggetti creati automaticamente dal container possono essere essenzialmente di due tipi: Singleton o Di istanza. Nel primo caso, il primo fruitore (risolutore) dell’oggetto, farà in modo che l’oggetto venga creato dal Container. Tutte le chiamate successive faranno riferimento al primo oggetto creato. Nel caso di oggetti “di istanza”, ad ogni chiamata di risoluzione di un determinato oggetto, verrà creata una nuova istanza dell’oggetto.
1: //Registering a service
2: this.container.RegisterType<IEmployeesController, EmployeesController>();
3: ...
4: //Resolving a presenter
5: EmployeesPresenter presenter = this.container.Resolve<EmployeesPresenter>();
Questo concetto promette di “sconvolgere” parecchio le abitudini della comune programmazione. Solitamente, infatti, siamo abituati a fare “new” delle entità quando ci serve, considerando ciclo di vita e scope di visibilità di un oggetto secondo le comuni regole. Utilizzando il container queste regole vengono modificate, e bisogna farci un pò l’abitudine. I vantaggi che hanno adottandolo in un ambiente disaccoppiato come quello di Prism, però, sono indubbi.
Conclusioni
Utilizzare il container sempre in modo cosciente e misurato:
- Se non è necessario, non utilizzare il container per non incorrere in overhead durante la creazione degli oggetti;
- Valutare sempre se un oggetto debba essere creato come singleton o instance; privilegiare la soluzione singleton per componenti cross-application quali autenticazione e logging;
- Valutare se configurare il container attraverso la configurazione o attraverso il codice; privilegiare la configurazione per modifiche flessibili, mentre impiegare il codice per configurazioni condizionali;
Come sempre, per approfondimenti rinvio alla documentazione di Prism. La potete scaricare qui.