IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Architecture en couches, découplage et injection de dépendances avec Unity


précédentsommairesuivant

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 :

 
Sélectionnez
_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 :

 
Sélectionnez
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 :

 
Sélectionnez
<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 :

 
Sélectionnez
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 :

 
Sélectionnez
<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é).

 
Sélectionnez
<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 :

 
Sélectionnez
public void Initialize()
{
    System.Diagnostics.Trace.Write("Hello from Initialize");
}
 
Sélectionnez
<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.

 
Sélectionnez
public string StoreFile { private get; set; }

public DataAccess()
{
}
 
Sélectionnez
<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.


précédentsommairesuivant

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2011 Nathanael Marchand. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.