Inversion Of Control, Dependency Injection: UNITY 2.1 (Parte 2)
Scritto da
Pietro Libro
il
mercoledì 29 giugno 2011
Linguaggio:
•
Framework:
•
Livello: 200
Download sorgenti
Nel primo articolo della serie abbiamo visto come
utilizzare Unity per iniettare una dipendenza tramite costruttore,
uno degli scenari più comuni nello sviluppo delle nostre
applicazioni. Vediamo in questa seconda parte come eseguire
DI tramite proprietà o metodi.
Property Injection
Modifichiamo il codice della classe BookShelf
aggiungendo una nuova proprietà:
public IBookReader BookReader
{
get
{
return _bookReader;
}
set
{
_bookReader = value;
}
}
Ed eliminiamo (o commentiamo) il costruttore che accetta come
unico parametro in ingresso un'istanza che implementa
IBookReader, lasciando solo il costruttore di
Default (senza parametri) della classe. Per istruire il
container quali sono le proprietà soggette ad "Iniezione" è
sufficiente decorare quest'ultime con l'attributo
[Dependency] (non necessario in caso di configurazione via
codice o XML):
[Dependency]
public IBookReader BookReader …
Quando chiameremo (ad esempio) Resolve per creare una
nuova istanza di BookShelf tramite Unity, verrà
automaticamente istanziata, secondo la configurazione caricata, un
tipo che implementa IBookReader ed iniettata in
BookShelf tramite il Setter della proprietà
BookReader. Eseguendo il codice abbiamo lo stesso
risultato che si ottiene utilizzando il codice dell'articolo
precedente:
container.RegisterType<BookShelf>(new InjectionProperty("BookReader"));
oppure
IBookReader ibookReader = container.Resolve<IBookReader>();
container.RegisterType<BookShelf>(new InjectionProperty("BookReader",ibookReader ));
Volendo, possiamo assegnare una specifica istanza di
IBookReader alla nuova istanza di BookShelf in
questo modo (un'alternativa è 'utilizzare il metodo
RegisterInstance per registrare un'istanza di oggetto con
nome, richiamabile successivamente, vedremo del codice di esempio
più avanti nell'articolo) oppure possiamo agire sul file di
configurazione XML:
<register type="BookShelf" mapTo="BookShelfConsole.BookShelf, BookShelfConsole">
<property name="BookReader" />
</register>
Method Injection
Proviamo ad eseguire DI tramite un metodo esposto da
BookShelf:
public void SetBookReader(IBookReader bookReader)
{
_bookReader = bookReader;
}
Come per le proprietà è sufficiente istruire Unity decorarndo il
metodo con un apposito attributo [InjectionMethod]:
[InjectionMethod]
public void SetBookReader(IBookReader bookReader)
Al solito, eseguiamo il codice per verificarne il corretto
funzionamento. Finora abbiamo considerato casi molti banali di
iniezione di codice, e come sappiamo, la realtà non è così
semplice. Complichiamoci la vita, aggiungendo più di un parametro
in ingresso per il costruttore ed il metodo soggetto ad iniezione.
Vediamo quindi come possiamo "addestrare" Unity a popolare
automaticamente i valori di questi parametri. Prendiamo in
esame un costruttore del tipo:
public BookShelf(IBookReader bookReader, string category, int capacity)
{
_capacity = capacity;
_category = category;
_bookReader = bookReader;
}
Se provassimo ad eseguire il codice così com'è, otterremo
un'eccezione a runtime:

Perché Unity si accorge di non avere sufficienti informazioni
nella registrazione del tipo mappato per poterne costruire uno.
Interveniamo sul file XML in questo modo:
<register type="BookShelf" mapTo="BookShelfConsole.BookShelf, BookShelfConsole">
<constructor>
<param name="bookReader">
<dependency/>
</param>
<param value="Informatic" name="category"/>
<param value="100" name="capacity" />
</constructor>
</register>
In esecuzione otterremmo:

Oltre che via XML, possiamo intervenire via codice utilizzando
il metodo InjectionConstructor durante la registrazione
del Container:
IBookReader ibookReader = container.Resolve<IBookReader>();
container.RegisterType<BookShelf>(new InjectionConstructor(ibookReader, "Informatic", 100));
BookShelf bs = container.Resolve<BookShelf>();
Ottenendo lo stesso risultato visto in precedenza. Questa
modalità di DI è comoda quando i valori da iniettare nel
costruttore della classe soggetta a dipendenza non sono noti a
Design-Time. Stesso discorso vale nel caso eseguissimo l'iniezione
di codice tramite un metodo:
public void SetBookReader(IBookReader bookReader, string category, int capacity)
{
_capacity = capacity;
_category = category;
_bookReader = bookReader;
}
Via codice:
IBookReader ibookReader = container.Resolve<IBookReader>();
container.RegisterType<BookShelf>(new InjectionMethod("SetBookReader", ibookReader, "Informatic", 100));
BookShelf bs = container.Resolve<BookShelf>();
E tramite configurazione XML:
<register type="BookShelf" mapTo="BookShelfConsole.BookShelf, BookShelfConsole">
<method name="SetBookReader">
<param name="bookReader">
<dependency/>
</param>
<param value="Informatic" name="category"/>
<param value="100" name="capacity" />
</method>
<register>
Unity ci permette di configurare i tre principali metodi di DI
in una molteplicità di modi, secondo le esigenze. Questa prima
parte dell'articolo non esaurisce tutte le possibili configurazione
di DI. Per chi volesse approfondire è disponibile ulteriore
documentazione su http://unity.codeplex.com/ (un PDF di 223 pagine
di 1,4 MB circa).
Sicuramente, una feature molto interessante, è la
possibilità di iniettare un array di valori ogni qual volta è
creata un'istanza di un tipo registrato. Supponiamo di aggiungere
alla nostra classe BookShelf un metodo SetBook
come descritto dal codice seguente:
public void SetBooks(IBook[] books)
{
_books = new List<IBook>(books.Count());
foreach (IBook b in books)
{
_books.Add(b);
}
}
Per inizializzare la nostra libreria con degli oggetti "Book" di
default, possiamo intervenire utilizzando la configurazione XML ( o
tramite codice fluent), utlizzando gli elementi
Array e TypeConverter in questo caso, per
convertire una stringa separata da ";" in un'istanza di oggetto che
implementa IBook. Registriamo un alias
per IBook e la classe TypeConverter, derivante da
System.ComponentModel.TypeConverter:
<alias alias="IBook" type="BookShelfContracts.IBook, BookShelfContracts"/>
<alias alias="BookConverter" type="BookShelfBusinessObjects.BookConverter, BookShelfBusinessObjects"/>
La classe BookConverter è così definita:
public class BookConverter : System.ComponentModel.TypeConverter
{
public override object ConvertFrom(System.ComponentModel.ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
string[] strs = value.ToString().Split(';');
return new Book() { Title = strs[0], ISBN = strs[1] };
}
}
questa non fa altro che prendere in ingresso la stringa che
definisce titolo ed ISBN del libro. L'istanza di Book
creata è aggiunta all'array di Book da iniettare a
runtime tramite il metodo SetBooks di
BookShelf. La configurazione XML è la seguente:
<register type="BookShelf" mapTo="BookShelfConsole.BookShelf, BookShelfConsole">
<method name="SetBooks">
<param name="books">
<array type="IBook">
<value value="Visual Studio 6.0;12345;" typeConverter="BookConverter"/>
</array>
</param>
</method>
</register>
Eseguendo il codice precedente dovremmo ottenere qualcosa di
questo tipo:

Generics
La registrazione dei tipi generici avviene nello stesso modo in
cui avviene per i tipi non generici. Supponiamo di definire la
seguente classe:
public class MyGenericClass<T>
{
T _injectedValue;
public MyGenericClass(T value)
{
_injectedValue = value;
}
}
Registriamo il tipo, specificando il tipo generico:
container.RegisterType(typeof(MyGenericClass<>));
E creiamo un'istanza richiamando il solito metodo
Resolve<> in questo modo:
MyGenericClass<IBook> genericBookClass = container.Resolve<MyGenericClass<IBook>>();
Proviamo a modifcare la nostra classe generica in questo
modo:
public class MyGenericClass<T> where T : IBook
{
T _injectedValue;
public MyGenericClass(T value, string title, string isbn)
{
_injectedValue = value;
_injectedValue.Title = title;
_injectedValue.ISBN = isbn;
}
public string ISBN { get { return _injectedValue.ISBN; } }
public string Title { get { return _injectedValue.Title; } }
}
Possiamo utilizzare un'istanza di GenericParameter per
specificare il parametro generico (in questo caso IBook)
ed utilizzare l'approccio visto in precedenza per passare i valori
per gli altri parametri:
container.RegisterType(typeof(MyGenericClass<>), new InjectionConstructor(new GenericParameter("T", "bookKey"), "New Book Title", "New ISBN"));
IBook ibookInstance = container.Resolve<IBook>();
container.RegisterInstance<IBook>("bookKey", ibookInstance);
MyGenericClass<IBook> genericBookClass = container.Resolve<MyGenericClass<IBook>>();
In questo caso utilizziamo il metodo
RegisterInstance<> per registrare un'istanza di
Ibook nel container. Questa metodo è molto simile ad avere
a disposizione un singleton della classe registrata, a
differenza che l'istanza non è automaticamente creata dal
container, ma è nostro compito istanziarla ed aggiungerla al
Container. Nella registrazione del tipo generico, nella
definizione tramite GenericParameter abbiamo
specificato come secondo argomento la chiave "bookKey", la quale è
utilizzata per registrare l'istanza IBookInstance
nel container. Nell'ultima riga del codice precedente, la chiamata
del metodo Resolve<…> utilizza l'istanza
iBookInstance per l'iniezione dei valori nel costruttore
della classe generica.
Lifetime
Unity offre un supporto notevole alla registrazione di classe,
interfacce, tipi generici ecc.., attraverso l'utilizzo dei metodi
RegisterType e RegisterInstance. Un altro
supporto, da non sottovalutare, è dato dai Lifetime
mangers, i quali permettono di gestire il ciclo di vita degli
oggetti istanziati , come ad esempio Singleton o per
thread.
Il comportamento di default di Unity è quello di creare una nuova
istanza di oggetto ogni qual volta viene chiamato il metodo
Resolve o ResolveAll. Se vogliamo che un
riferimento agli oggeti creati sia memorizzato nel container è
necessario istruire Unity in modo opportuno, ad esempio utilizzando
RegisterInstance come visto precedentemente in questo
articolo.
Unity comprende se Lifetime Manager Built-In, con la
possibilità di crearne dei custom se abbiamo uno scenario che lo
richiede:
- TransientLifeTimeManager: (Manager di Default
per RegisterType) Per ogni chiamata a Resolve o
ResolveAll unity crea una nuova istanza di oggetto.
- ContainerControlledLifetimeManager: registra
un oggetto esistente come un singleton. La stessa istanza di
oggetto è ritornata per ogni chiamata a Resole e
ResolveAll. Questo Lifetime Manager è utilizzato di
default quando viene utilizzato il metodo RegisterInstance
se non è specificato un Lifetime manager diverso. Se non
diversamente specificato nella configurazione o tramite
RegisterType, Unity crea una nuova istanza del metodo
registrato durante la prima chiamata del meccanismo di DI e poi
ritorna sempre la stessa istanza.
- HierarchicalLifetimeManager: come per
ContainerControlledLifetimeManager, Unity ritorna la
stessa istanza di oggetto ogni volta che uno dei metodi
Resolve o ResolveAll è chiamato, o quando il
meccanismo di DI inietta un'istanza in una classe dipendente. A
differenza di ContainerControlledLifetimeManager, nel caso
di container padre-figlio ogni figlio risolve la propria
istanza di oggetto, senza nessuna condivisione con il padre, quindi
ogni container gestisce la propria istanza.
- PerResolveLifetimeManager: questo
gestore è simile al TransientLifetimeManager, ma
fornisce un meccanismo per il quale un'istanza di oggetto può
essere riutilizzata durante la costruzione di un grafo di
oggetti.
- PerThreadLifetimeManager: Il
comportamento di questo manager è quello di ritornare, sulle base
di ogni thread, la stessa istanza del tipo o oggetto registrato per
ogni chiamata al metodo Resolve o ResolveAll, o
quando avviene il meccanismo di DI per l'iniezione dell'istanza in
una classe dipendente. Il comportamento è quello del Singleton,
differente per ogni thread che ha accesso al container.
- ExternallyControlledLifetimeManager:
questo manager fornisce supporto a Lifetime manager esterni
nel caso il nostro scenario di utilizzo di Unity abbia esigenze
tali per cui nessuno dei manager visti in precedenza possa essere
utilizzato. E' sempre possibile registrare un tipo o un'istanza di
oggetto con il container, ma è mantenuta solo una weak reference
all'oggetto in modo tale che sia il nostro codice a occuparsi del
ciclo di vita dell'oggetto istanziato (ad esempio
mantenendolo in memoria piuttosto che eseguirne il Dispose)
tramite Resolve, ResolveAll o tramite il
meccanismo di DI. Importante: dato che il container non ha una
riferimento forte (strong reference) sull'oggetto dopo
averlo creato, il garbage collector può eseguire il
Dispose se nessun codice ha uno strong reference
con esso.
Ad esempio, se volessimo creare un singleton della nostra
istanza di BookShelf, interagendo tramite configurazione
XML, scriveremmo qualcosa di questo tipo:
<register type="BookShelf" mapTo="BookShelfConsole.BookShelf, BookShelfConsole">
<lifetime type="singleton"/>
Da codice:
BookShelf bs1 = container.Resolve<BookShelf>();
BookShelf bs2 = container.Resolve<BookShelf>();
Eseguendo:

Concludiamo così la seconda parte dell'articolo
introduttivo ad Unity 2.1 e alle funzionalità di base.
Sicuramente gli argomenti non sono stati trattati in modo esaustivo
e non tutte le funzionalità di Unity sono state prese in
considerazione (come ad esempio Interception, argomento di un
prossimo articolo), ma speriamo siano stati sufficienti a stimolare
la curiosità nell'adottare un framework IoC Come Unity.
Tags: IoC,Dependency Injection,Unity