IV. Unity, configuration avancée▲
IV-A. Le pattern ServiceLocator▲
Comme nous avons pu le voir à la section précédente, il est possible de résoudre un type ou une interface à l'aide d'une instance d'un container Unity. Cependant, bien que très pratique, cette solution exige de toujours avoir sous la main une référence vers le container Unity.
Heureusement, le pattern ServiceLocator peut nous aider. C'est une sorte de singleton vers notre container. Il agit également comme une façade : il cache l'implémentation Unity et permet éventuellement l'utilisation d'autres gestionnaires d'injection de dépendances de manière transparente pour le code.
L'utilisation se fait en deux temps : il faut dans un premier temps définir notre container comme le fournisseur du ServiceLocator et c'est après que nous pourrons l'utiliser.
Voici ce que devient la configuration de Unity :
_container =
new
UnityContainer
(
).
LoadConfiguration
(
);
ServiceLocator.
SetLocatorProvider
((
) =>
new
UnityServiceLocator
(
_container));
Le paramètre de SetLocatorProvider est un délégué qui retourne une instance implémentant IServiceLocator. Ici, une instance de UnityServiceLocator fait l'affaire. Il est également possible de créer sa propre implémentation de IServiceLocator et ainsi d'utiliser Spring.Net par exemple à la place de Unity.
À l'utilisation, on peut donc supprimer l'attribut _container de notre classe Program et utiliser le ServiceLocator comme ceci :
private
static
void
TreatChoice
(
int
i)
{
var
businessLayer =
ServiceLocator.
Current.
GetInstance<
IBusinessLogic>(
);
String country;
ushort
year;
List<
Movie>
movies;
switch
(
i)
{
case
1
:
movies =
businessLayer.
GetMovies
(
);
foreach
(
var
movie in
movies)
System.
Console.
WriteLine
(
"{0}: {1} - {2} - {3}"
,
movie.
Id,
movie.
Title,
movie.
Year,
movie.
Country);
break
;
case
2
:
System.
Console.
WriteLine
(
"Enter year:"
);
year =
ushort
.
Parse
(
System.
Console.
ReadLine
(
));
movies =
businessLayer.
GetMoviesByYear
(
year);
foreach
(
var
movie in
movies)
System.
Console.
WriteLine
(
"{0}: {1} - {2} - {3}"
,
movie.
Id,
movie.
Title,
movie.
Year,
movie.
Country);
break
;
case
3
:
System.
Console.
WriteLine
(
"Enter country:"
);
country =
System.
Console.
ReadLine
(
);
movies =
businessLayer.
GetMoviesByCountry
(
country);
foreach
(
var
movie in
movies)
System.
Console.
WriteLine
(
"{0}: {1} - {2} - {3}"
,
movie.
Id,
movie.
Title,
movie.
Year,
movie.
Country);
break
;
case
4
:
System.
Console.
WriteLine
(
"Enter year:"
);
year =
ushort
.
Parse
(
System.
Console.
ReadLine
(
));
System.
Console.
WriteLine
(
"Enter country:"
);
country =
System.
Console.
ReadLine
(
);
movies =
businessLayer.
GetMoviesByYearAndCountry
(
year,
country);
foreach
(
var
movie in
movies)
System.
Console.
WriteLine
(
"{0}: {1} - {2} - {3}"
,
movie.
Id,
movie.
Title,
movie.
Year,
movie.
Country);
break
;
}
}
L'utilisation du ServiceLocator comporte quand même quelques dangers. Ainsi il est possible de ne plus utiliser l'injection sur constructeur et de tout faire avec le ServiceLocator, mais cela pourrait nous amener une plus faible lisibilité du code (lorsqu'on voit les paramètres du constructeur, on comprend vite les dépendances de la classe) et des problèmes de boucle infinie lors de dépendances circulaires.
IV-B. Gestion de la durée de vie▲
Si l'on place un point d'arrêt au niveau du constructeur de notre BusinessLayer, on s'aperçoit qu'à chaque Resolve() du container, on a une nouvelle instance créée. En effet, par défaut, c'est le comportement de Unity. Il est cependant possible, comme le spécifie le manuel, de régler ce comportement. Voici les comportements possibles :
Nom |
Description du comportement |
---|---|
TransientLifetimeManager |
Le comportement par défaut : à chaque Resolve, une nouvelle instance |
ContainerControlledLifetimeManager |
Fonctionnement en mode singleton : Le premier Resolve crée et retourne un objet et tous les autres retournent cette instance |
HierarchicalLifetimeManager |
Il est possible d'avoir plusieurs containers et de les hiérarchiser (notion non abordée dans cet article). Contrairement à ContainerControlledLifetimeManager où un père partage ses instances avec ses enfants, ici chacun possède ses propres instances. |
PerResolveLifetimeManager |
Similaire à TransientLifetimeManager, une instance par Resolve. Cependant, c'est la même instance dans tout le graphe d'appel (cf. Exemple de la documentation). |
PerThreadLifetimeManager |
Semblable à ContainerControlledLifetimeManager : C'est un singleton, mais par thread. Deux Resolve dans deux threads résoudront deux instances différentes, deux Resolve dans le même thread résoudront la même. |
ExternallyControlledLifetimeManager |
Unity ne gère pas la vie de l'objet, il ne maintient qu'une WeakReference vers l'objet afin qu'il puisse être collecté par le GarbageCollector. |
Les configurations les plus utilisées sont le TransientLifetimeManager (puisque comportement par défaut), le ContainerControlledLifetimeManager et le PerResolveLifetimeManager.
La configuration se fait par Mapping dans le fichier de configuration. Dans notre application, nous allons configurer la couche d'accès aux données en mode ContainerControlledLifetimeManager. Ainsi, à chaque appel de la couche Business, une nouvelle instance de business sera fournie, mais avec toujours la même instance de la couche d'accès aux données. Cette configuration se fait dans le fichier de configuration :
<container>
<register
type
=
"IDataAccess"
mapTo
=
"DataAccess"
>
<lifetime
type
=
"singleton"
/>
</register>
<register
type
=
"IBusinessLogic"
mapTo
=
"BusinessLogic"
/>
</container>
IV-C. Les injections non triviales▲
Jusqu'à présent, nous avons utilisé des cas de figure assez simples : injection d'un paramètre dans le constructeur et unicité du constructeur.
Imaginons que désormais, nous souhaitions pouvoir configurer notre couche d'accès aux données et par exemple passer le nom du fichier de données en paramètre au constructeur. Néanmoins, nous décidons d'avoir quand même un constructeur sans paramètres avec une valeur par défaut.
Notre déclaration de StoreFile devient donc :
public
string
StoreFile;
public
DataAccess
(
)
{
StoreFile =
"store.xml"
;
}
public
DataAccess
(
String filename)
{
StoreFile =
filename;
}
Si on lance l'application : PATATRAS ! Erreur ! Unity ne sait pas quel constructeur utiliser. De plus, s’il utilise le second, il ne sait pas quoi passer en paramètre. Ça n'est pas grave, nous allons lui expliquer. Tout d'abord, nous allons lui dire d'utiliser le constructeur sans paramètre. Cela se fait de manière très simple :
<register
type
=
"IDataAccess"
mapTo
=
"DataAccess"
>
<lifetime
type
=
"singleton"
/>
<constructor/>
</register>
Présente sans aucun nœud fils, la balise constructor permet de dire d'utiliser le constructeur sans paramètre. Mais nous pouvons demander le constructeur avec un paramètre. Il suffit d'utiliser la balise param, elle possède un attribut obligatoire : le nom du paramètre. Si elle possède un attribut value, c'est cette valeur qui est passée (utile pour des entiers, des chaines de caractères, etc.). Si elle n'en possède pas, la valeur du paramètre est injectée (et le type doit donc être mappé).
<register
type
=
"IDataAccess"
mapTo
=
"DataAccess"
>
<lifetime
type
=
"singleton"
/>
<constructor>
<param
name
=
"filename"
value
=
"store.xml"
/>
</constructor>
</register>
Une autre possibilité de Unity, c'est l'appel d'une méthode juste après le constructeur pour par exemple initialiser l'objet. Essayons de créer une méthode Initialize dans notre couche d'accès aux données :
public
void
Initialize
(
)
{
System.
Diagnostics.
Trace.
Write
(
"Hello from Initialize"
);
}
<register
type
=
"IDataAccess"
mapTo
=
"DataAccess"
>
<lifetime
type
=
"singleton"
/>
<constructor>
<param
name
=
"filename"
value
=
"store.xml"
/>
</constructor>
<method
name
=
"Initialize"
/>
</register>
Le fonctionnement est identique à l'injection sur constructeur : on peut utiliser la balise param pour passer les paramètres. La méthode doit obligatoirement être publique !
L'injection de propriétés peut être utile pour les problèmes de dépendances circulaires. Les propriétés doivent avoir un accesseur set public pour que cela fonctionne.
public
string
StoreFile {
private
get
;
set
;
}
public
DataAccess
(
)
{
}
<register
type
=
"IDataAccess"
mapTo
=
"DataAccess"
>
<lifetime
type
=
"singleton"
/>
<constructor />
<property
name
=
"StoreFile"
value
=
"Store.xml"
/>
</register>
La documentation d'Unity regorge d'autres surprises ! Par exemple les paramètres optionnels, le passage de tableaux, les convertisseurs de types de paramètres, etc. Le mieux pour tout découvrir reste de la lire.