Inversion Of Control, Dependency Injection: UNITY 2.1 (Parte 2)

Scritto da  Pietro Libro il mercoledì 29 giugno 2011
Linguaggio: C#   •  Framework: 3.5,4.0   •  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:

Figura 5

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:

Figura 6

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:

Figura 7

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:

Figura 8

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

 
x