www.bewise.fr

Recherche

Patterns & Practices – Unity 2.0 Part 1

Par Franck Lizzi-Chardon, posté le 23/05/2011

Profil : Développeur | Niveau : Intermédiaire (200)

Tags : C#, Design pattern | Partager : Partager sur Delicious Partager sur Facebook Partager sur Twitter

Unity est un conteneur léger d'injection de dépendances. Il a pour but de simplifier l'architecture d'une application dans le cas de fortes dépendances entre composants. Il implémente principalement les deux paradigmes suivant :

· L'IOC (Inversion Of Control)

· L'AOP (Aspect Oriented Programming)

Dans ce premier article, nous allons voir ensemble comment installer et mettre en place unity dans une application minimaliste.

 

Unity en quelques mots

 

Pourquoi l'IOC ?

L'IOC ou l'inversion de contrôle est une méthode pour rendre plus modulable une application. En effet, l'approche première d'un développeur est en général de piloter le Framework par le code. Dans l'approche IOC, le flot d'exécution n'est plus sous le contrôle direct de l'application, mais du Framework utilisé.

L'IOC existe déjà dans le Framework .Net. Nous pouvons le retrouver par exemple dans le pattern évènementiel. En effet, quand on implémente un clic de bouton dans une application WPF ou Winforms, on fournit un bloc de code au Framework et c'est ce dernier qui se charge de l'appeler au bon moment.

Ce principe se retrouve directement dans le Framework Unity. Plutôt que de créer des objets par le code, nous allons fournir toutes les informations nécessaires au Framework et lui laisser faire tout le travail. Dans le cas d'objets complexes qui contiennent des références sur d'autres objets, Unity va être capable de gérer ces références de façon interne pour renvoyer un objet complet. C'est pourquoi le Framework Unity est aussi connu comme étant un conteneur d'injection de dépendances.

 

Comment ça fonctionne ?

Unity va gérer l'instanciation de nos objets. Nous allons préciser par configuration le comportement que le Framework doit adopter. L'idée est de ne manipuler que des interfaces, et de préciser dans la configuration quelle sera l'implémentation réelle. De cette façon, nous allons pouvoir changer le type des objets instanciés sans même recompiler le code. De la même façon, nous allons pouvoir gérer les dépendances entres objets, c'est-à-dire laisser le Framework injecter les objets dépendants

 

Mise en place du Framework

 

Installation

Nous allons commencer par installer la dernière version du Framework Unity. Elle est disponible sur codeplex : http://unity.codeplex.com/

Créez un nouveau projet et rajoutez les références vers Microsoft.Practices.Unity et Microsoft.Practices.Unity.Configuration.

 

clip_image002

 

Mise en place d'une interface et des implémentations

Nous allons créer une interface, et 2 classes implémentant cette interface. Notre interface s'appelle IBackupService, c'est une interface qui expose une méthode permettant de sauvegarder un objet.

 

IBackupService.cs :

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.Runtime.Serialization;
   6:  
   7: namespace ConsoleApplication1
   8: {
   9:     interface IBackupService
  10:     {
  11:         void BackupData(ISerializable obj);
  12:     }
  13: }

 

Nous allons ensuite proposer deux implémentations distinctes de cette interface, FileBackupService et AzureBackupService.

La classe AzureBackupService permet de sauvegarder le contenu d'un objet serializable dans le Cloud Azure. Dans cet exemple, nous n'allons pas détailler l'implémentation, ce n'est pas l'objectif de cet article.

 

AzureBackupService.cs :

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.Runtime.Serialization;
   6:  
   7: namespace ConsoleApplication1
   8: {
   9:     class AzureBackupService: IBackupService
  10:     {
  11:         public AzureBackupService()
  12:         { }
  13:  
  14:         public void BackupData(ISerializable obj)
  15:         {
  16:             Console.WriteLine("objet inséré dans le Cloud");
  17:         }
  18:     }
  19: }

 

La classe FileBackupService permet de sauvegarder le contenu d'un objet serializable sur le système de fichier.

 

FileBackupService.cs :

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.Runtime.Serialization;
   6:  
   7: namespace ConsoleApplication1
   8: {
   9:     class FileBackupService: IBackupService
  10:     {
  11:         public FileBackupService()
  12:         { }
  13:  
  14:         public void BackupData(ISerializable obj)
  15:         {
  16:             Console.WriteLine("objet inséré dans le système de fichiers");
  17:         }
  18:     }
  19: }

 

Configuration du framework Unity

Nous allons maintenant configurer unity. Il est possible de configurer le Framework par programmation et par déclaration. Nous allons opter pour cette dernière syntaxe puisqu'elle va nous permettre de changer le comportement du Framework sans recompiler le code.

 

App.config :

   1: <xml version="1.0"?>
   2: <configuration>
   3:   <configSections>
   4:     <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,                Microsoft.Practices.Unity.Configuration, Version=2.0.414.0,                Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
   5:   </configSections>
   6:   <unity>
   7:     <typeAliases>
   8:       <typeAlias alias="IBackupService" type="ConsoleApplication1.IBackupService, ConsoleApplication1"/>
   9:       <typeAlias alias="FileBackupService" type="ConsoleApplication1.FileBackupService, ConsoleApplication1"/>
  10:       <typeAlias alias="AzureBakupService" type="ConsoleApplication1.AzureBakupService, ConsoleApplication1"/>
  11:     </typeAliases>
  12:     <containers>
  13:       <container name="containerOne">
  14:         <types>
  15:           <type type="IBackupService" mapTo="FileBackupService">
  16:           </type>
  17:         </types>
  18:       </container>
  19:     </containers>
  20:   </unity>
  21:   <startup>
  22:     <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
  23:   </startup>
  24: </configuration>

 

Voici la description des différentes sections

  • « TypeAliases » : Cette section permet de préciser des simplifications d'écriture pour les types référencés plus bas.
  • « Container » : On enregistre dans chaque container la liste des types, ainsi que leur mapping vers les types à instancier.

 

Utilisation du Framework

Dans un premier temps, nous allons initialiser le container unity grâce au fichier de configuration. Ce code va s'exécuter au démarrage de l'application. Ensuite, pour des raisons de facilité, on enregistre généralement le container unity dans la variable Application ou dans un singleton afin de garder un pointeur dessus tout au long du cycle de vie du programme.

   1: UnityContainer myContainer = new UnityContainer(); 
   2: UnityConfigurationSection section =  (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
   3: section.Configure(myContainer, "containerOne");

Il ne reste plus qu'à utiliser le container pour récupérer une instance d'IbackupService

   1: IBackupService myExampleInstance = myContainer.Resolve<IBackupService>();

Ici nous utilisons la méthode Resolve() du container pour obtenir un IbackupService. Dans l'exemple suivant, nous allons récupérer un FileBackupService.

 

Règles de Nommage

Unity offre également la possibilité de nommer un mapping afin d'avoir plusieurs possibilités de routage pour une même interface. Il suffit de rajouter la propriété « name » sur la balise type dans le fichier de configuration :

   1: <type type="IBackupService" mapTo="FileBackupService">
   2: </type>
   3: <type type="IBackupService" mapTo="FileBackupService" name="StandardBackup">
   4: </type>
   5: <type type="IBackupService" mapTo="AzureBakupService" name="InternetBackup">
   6: </type>

 

Utilisation :

   1: IBackupService IBackupServiceInstance;
   2: // Retourne un FileBackupService
   3: IBackupServiceInstance = myContainer.Resolve();
   4: // Retourne un FileBackupService
   5: IBackupServiceInstance = myContainer.Resolve("StandardBackup");
   6: // Retourne un AzureBackupService
   7: IBackupServiceInstance = myContainer.Resolve("InternetBackup");

 

Injection de dépendances et cycle de vie

 

Unity ayant pris en charge la création de nos objets, nous n'avons plus la main sur leur instanciation, en effet, le Framework prend en charge la gestion du mot clé « new ». 2 problèmes se posent alors :

  • Comment passer des paramètres aux objets lors de leurs initialisations.
  • Comment gérer le cycle de vie des objets

 

L'injection de dépendances

L'injection de dépendance va permettre au Framework d'initialiser nos instances avec des paramètres. En effet la méthode Resolve() est récursive, elle va être également appelée sur tous les types nécessaires à la création de notre objet.

Nous allons complexifier la classe « FileBackupService »

 

FileBackupService.cs :

   1: public class FileBackupService : IBackupService
   2: {
   3:     private FileBackupConfiguration _config;
   4:  
   5:     public FileBackupService(FileBackupConfiguration config)
   6:     {
   7:         _config = config;
   8:     }
   9:  
  10:     public void BackupData(ISerializable obj)
  11:     {
  12:         Stream stream = File.Open(_config.fileName, FileMode.Create);
  13:         _config.formater.Serialize(stream, obj);
  14:         stream.Close();
  15:     }
  16: }

 

Cette classe va maintenant prendre un objet de configuration en paramètre (à passer dans le constructeur comme le présente le code ci-dessus) :

 

FileBackupConfiguration.cs :

   1: public class FileBackupConfiguration
   2: {
   3:     public string fileName { get; set; }
   4:     public IFormatter formater { get; set; }
   5:  
   6:     public FileBackupConfiguration(IFormatter f)
   7:     {
   8:         fileName = "c:\\backup.txt";
   9:         formater = f;
  10:     }
  11: }

 

La méthode de backup va utiliser le formatter contenu dans l'objet de configuration pour sérialiser l'objet. Mais nous ne voulons pas décider à ce moment quel type de formatter nous allons utiliser ! Car bien entendu, Unity est aussi capable de résoudre les types existants du Framework.

Voilà quel va être le workflow de la résolution de l'interface IBackupService :

  1. Résolution d'IBackupService -> FileBackupService
  2. Résolution de FileBackupService -> Besoin de résoudre le constructeur
  3. Résolution du constructeur de FileBackupService -> Besoin de résoudre FileBackupConfiguration
  4. Résolution de FileBackupConfiguration -> Besoin de résoudre le constructeur
  5. Résolution du constructeur de FileBackupConfiguration -> Besoin de résoudre IFormatter
  6. Résolution de IFormatter -> OK

Il ne reste plus qu'à configurer notre conteneur pour que cela fonctionne :

 

App.config :

   1: <xml version="1.0"?>
   2: <configuration>
   3:   <configSections>
   4:     <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,                Microsoft.Practices.Unity.Configuration, Version=2.0.414.0,                Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
   5:   </configSections>
   6:   <unity>
   7:     <typeAliases>
   8:       <typeAlias alias="BinaryFormatter" type="System.Runtime.Serialization.Formatters.Binary.BinaryFormatter, mscorlib"/>
   9:       <typeAlias alias="SoapFormatter" type="System.Runtime.Serialization.Formatters.Soap.SoapFormatter, System.Runtime.Serialization.Formatters.Soap"/>
  10:       <typeAlias alias="IFormatter" type="System.Runtime.Serialization.IFormatter, mscorlib"/>   
  11:       <typeAlias alias="singleton" type="Microsoft.Practices.Unity.ContainerControlledLifetimeManager, Microsoft.Practices.Unity"/>
  12:       <typeAlias alias="external" type="Microsoft.Practices.Unity.ExternallyControlledLifetimeManager, Microsoft.Practices.Unity"/>
  13:       <typeAlias alias="FileBackupConfiguration" type="ConsoleApplication1.FileBackupConfiguration, ConsoleApplication1"/>
  14:       <typeAlias alias="IBackupService" type="ConsoleApplication1.IBackupService, ConsoleApplication1"/>
  15:       <typeAlias alias="FileBackupService" type="ConsoleApplication1.FileBackupService, ConsoleApplication1"/>
  16:       <typeAlias alias="AzureBakupService" type="ConsoleApplication1.AzureBakupService, ConsoleApplication1"/>
  17:     </typeAliases>
  18:     <containers>
  19:       <container name="containerOne">
  20:         <types>
  21:           <type type="IFormatter" mapTo="SoapFormatter">
  22:             <constructor>
  23:             </constructor>
  24:             <lifetime type="external"/>
  25:           </type>
  26:           <type type="FileBackupConfiguration" mapTo="FileBackupConfiguration">
  27:             <lifetime type="singleton"/>
  28:           </type>
  29:           <type type="IBackupService" mapTo="FileBackupService">
  30:             <lifetime type="external"/>
  31:           </type>
  32:         </types>
  33:       </container>
  34:     </containers>
  35:   </unity>
  36:   <startup>
  37:     <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
  38:   </startup>
  39: </configuration>

 

Voilà la configuration terminée, l'appel à la méthode « Resolve() » aura pour effet de renvoyer un objet de type FileBackupService initialisé avec un Iformatter de type « SoapFormater ».

Il est important de bien mettre la balise « constructor » en dessous du type IFormatter. Cette balise va préciser à Unity d'utiliser le constructeur par défaut sans paramètre. Sans cette balise, le Framework va tenter de résoudre les types internes liés à la classe SoapFormatter.

 

Gestion du cycle de vie des objets

La balise « lifetime » permet de préciser lors de la configuration quelle est la politique à adopter pour gérer le cycle de vie de chaque objet.

Unity inclut six gestionnaires de cycle de vie (lifetime managers). Voici leur description :

  • TransientLifetimeManager : C'est le manager par défaut, il va renvoyer une nouvelle instance de l'objet demandé à chaque appel à la méthode Resolve().
  • ContainerControlledLifetimeManager : Ce manager implémente un singleton. Chaque appel à la méthode resolve() retournera la même instance de l'objet. Le constructeur sera appelé lors du premier appel, ou possibilité d'enregistrer une instance grâce à la méthode « RegisterInstance »
  • PerThreadLifetimeManager : Une instance est retournée pour chaque thread de l'application. Chaque instance est alors gérée comme un singleton.
  • HierarchicalLifetimeManager : même comportement que « ContainerControlledLifetimeManager ». Sauf que dans le cas d'une structure de container hiérarchique (relation parent enfant), chaque container aura sa propre instance de l'objet (et pas un seul partagé par tous).
  • PerResolveLifetimeManager : Ce manager a été créé pour gérer les cas de dépendance entre graphe d'objet. Ce manager a le même comportement que le « TransientLifetimeManager ». Il va cependant être capable de réutiliser une instance déjà créée pour éviter de tom ber dans une boucle infinie (Si A référence un B et que B référence un A, ..)
  • ExternallyControlledLifetimeManager : Ce manager permet de gérer par le code le cycle de vie de l'objet. Il fonctionne de la même façon qu'un « ContainerControlledLifetimeManager » (par singleton), seulement il ne maintient qu'une weak référence sur l'objet. Il appartient donc au développeur de garder un pointeur sur l'objet pour continuer à réutiliser cette même instance.

 

Conclusion

Unity va permettre grâce à l'injection de dépendance d'éliminer les couplages forts de vos applications pour obtenir un code plus testable et facilement modulable. Dans un prochain article je vous montrerais une autre facette de ce Framework : la programmation par aspect.

Consultant sur les technologies Microsoft

Voir les autres publications de l'auteur


Commentaires