III. Unity, principe et mise en œuvre simple▲
III-A. Présentation▲
Unity peut se configurer de deux façons différentes : par code ou par fichier de configuration. Afin d'être plus flexible, le fichier de configuration est souvent utilisé. Cependant, dans des cas comme une application Silverlight où la notion de fichier de configuration n'existe pas, la déclaration par code est utilisée.
On peut voir Unity comme une usine à objet : on demande un objet d'un certain type ou implémentant une certaine interface et Unity nous retourne une implémentation obéissant à plusieurs stratégies (cela peut être un singleton, une nouvelle entité à chaque appel, une entité par thread, etc.).
Ces entités sont stockées dans des containers.
III-B. Configuration▲
Il faut tout d'abord ajouter les références vers les dll Unity dans le projet console :
- Microsoft.Pratices.Unity ;
- Microsoft.Pratices.Unity.Configuration.
Il faut ensuite créer un fichier de configuration App.Config. Unity peut également utiliser un autre fichier pour la configuration. C'est utile dans une grosse application lorsqu'il y a beaucoup d'entités à déclarer.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
</configuration>
Le premier élément à ajouter est l'import de la balise unity, cela se fait en rajoutant :
<configSections>
<section
name
=
"unity"
type
=
"Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"
/>
</configSections>
On peut désormais utiliser la balise Unity dans notre configuration et déclarer nos entités dedans.
Astuce! Il est possible de déclarer l'utilisation du namespace unity lors de la déclaration de la balise unity. Ainsi, l'autocomplétion est disponible.
<unity
xmlns
=
"http://schemas.microsoft.com/practices/2010/unity"
>
</unity>
Nous allons dans un premier temps déclarer les Assemblies et Namespaces dans lesquels Unity doit trouver nos types et nos interfaces à instancier. Pour déclarer une assembly, il suffit d'utiliser une balise assembly et pour un namespace c'est la balise namespace. Dans notre exemple, il nous faut utiliser les assemblies et namespaces suivants :
- MovieBase.Business.Interfaces ;
- MovieBase.DataAccess.Interfaces ;
- MovieBase.Business ;
- MovieBase.XmlDataAccess.
<unity
xmlns
=
"http://schemas.microsoft.com/practices/2010/unity"
>
<assembly
name
=
"MovieBase.Business.Interfaces"
/>
<assembly
name
=
"MovieBase.Business"
/>
<assembly
name
=
"MovieBase.DataAccess.Interfaces"
/>
<assembly
name
=
"MovieBase.XmlDataAccess"
/>
<namespace
name
=
"MovieBase.Business.Interfaces"
/>
<namespace
name
=
"MovieBase.Business"
/>
<namespace
name
=
"MovieBase.DataAccess.Interfaces"
/>
<namespace
name
=
"MovieBase.XmlDataAccess"
/>
</unity>
Une fois cette étape effectuée, il faut déclarer nos entités avec éventuellement un mapping : on peut soit déclarer une interface et lui associer une classe d'implémentation (c'est un mapping), soit déclarer directement une classe. La déclaration de classe est cependant peu utilisée : elle brise le découplage.
Ces déclarations se font à l'intérieur d'une balise container. Voici ce que donne le fichier de configuration résultant :
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section
name
=
"unity"
type
=
"Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"
/>
</configSections>
<unity
xmlns
=
"http://schemas.microsoft.com/practices/2010/unity"
>
<assembly
name
=
"MovieBase.Business.Interfaces"
/>
<assembly
name
=
"MovieBase.Business"
/>
<assembly
name
=
"MovieBase.DataAccess.Interfaces"
/>
<assembly
name
=
"MovieBase.XmlDataAccess"
/>
<namespace
name
=
"MovieBase.Business.Interfaces"
/>
<namespace
name
=
"MovieBase.Business"
/>
<namespace
name
=
"MovieBase.DataAccess.Interfaces"
/>
<namespace
name
=
"MovieBase.XmlDataAccess"
/>
<container>
<register
type
=
"IDataAccess"
mapTo
=
"DataAccess"
/>
<register
type
=
"IBusinessLogic"
mapTo
=
"BusinessLogic"
/>
</container>
</unity>
</configuration>
Il ne reste plus qu'à utiliser ceci dans le code !
III-C. Utilisation▲
La première chose à faire est de supprimer le membre BusinessLogic de notre classe Program. Nous allons plutôt utiliser un membre de type IUnityContainer qui sera le container de nos entités. Puis il faut le configurer et enfin l'utiliser pour obtenir une instance de la couche business.
La configuration du container se fait en créant une nouvelle instance d'un UnityContainer puis en appelant la méthode d'extension LoadConfiguration().
Pour obtenir une instance implémentant IBusinessLogic, il suffit d'appeler la méthode d'extension Resolve<T>() sur le container.
Voici le code modifié pour notre utilisation :
class
Program
{
private
static
IUnityContainer _container;
static
void
Main
(
string
[]
args)
{
//Unity configuration
_container =
new
UnityContainer
(
).
LoadConfiguration
(
);
while
(
true
)
{
DisplayMenu
(
);
var
choice =
System.
Console.
ReadLine
(
);
if
(
choice ==
"q"
)
break
;
int
nChoice;
if
(!
int
.
TryParse
(
choice,
out
nChoice))
System.
Console.
WriteLine
(
"Choix inconnu!"
);
else
TreatChoice
(
nChoice);
System.
Console.
ReadLine
(
);
}
}
private
static
void
TreatChoice
(
int
i)
{
var
businessLayer =
_container.
Resolve<
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
;
}
}
private
static
void
DisplayMenu
(
)
{
System.
Console.
Clear
(
);
System.
Console.
WriteLine
(
"1 to display all"
);
System.
Console.
WriteLine
(
"2 to filter by year"
);
System.
Console.
WriteLine
(
"3 to filter by country"
);
System.
Console.
WriteLine
(
"4 to filter by year & country"
);
System.
Console.
WriteLine
(
"q to quit"
);
}
}
On remarque que contrairement à la première version, nous ne nous sommes pas préoccupés de l'instanciation de la couche d'accès aux données. Unity s'est aperçu que pour générer une instance de la couche business, le constructeur avait besoin d'une implémentation d'une couche d'accès aux données. On dit qu'il avait une dépendance dessus. Il a donc dans un premier temps généré une instance de la couche d'accès aux données, puis il l'a passée au constructeur de la couche business. C'est l'injection de dépendances ! Pour vous en convaincre, vous pouvez poser des points d'arrêts sur les différents constructeurs.
Attention aux références! En effet, on s'aperçoit qu'à aucun moment du code on utilise explicitement les types des assemblies Business et XmlDataAccess. Il serait tentant de supprimer les références projets ce qui serait totalement valable et justifié. Cependant, je préfère les garder pour bénéficier du build en cascade et de la recopie automatique dans le dossier de sortie. Si les références sont enlevées, il faut penser à copier manuellement les assemblies dans le dossier de sortie.
Désormais, notre application fonctionne, est découplée et utilise l'injection de dépendances. Nous allons pousser les expériences en découvrant le pattern de ServiceLocator, jouer sur les durées de vie de nos entités et voir les implémentations dans les différents projets possibles.