Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Injection de dépendance: tutoriel Unity

Poster un commentaire

Edit: C’est la deuxième version d’un autre article que je ne trouvais pas terrible après l’avoir relu. Alors je l’ai pas mal remanié et complété.

 

Après avoir fait un petit rappel sur ce qu’est l’injection de dépendance, un petit tuto sur un framework que j’aime bien: Unity.

On peut consulter le source sur Github :

https://github.com/unitycontainer/unity

 

Mais le moyen le plus simple d’ajouter Unity à ses projet est tout de même Nuget. Donc au programme:

  • Hello world
  • Constructeurs paramétrés
  • Scénario avancés avec plusieurs schémas
  • Injection de propriétés
  • Cycle de vie des instances
  • Cas des applications MVC et Web API

 

Le « Hello world »

Note: Dans cet article je n’aborderai pas une possibilité qui est de paramétrer Unity depuis le fichier de configuration. Cette solution ouvre la possibilité de faire des changements sans recompiler ce qui peut être utile. Mais c’est assez laborieux d’un point de vue pratique je trouve. Bref je ne suis pas fan.

 

Un tuto démarre toujours ainsi, suivons donc la tradition. Nous allons créer les classes suivantes dans un projet Console.

On commence par une interface:

interface IClasse
{
   void LireDonnees();
}

Puis les deux classes suivantes:

class ClasseFichier : IClasse
{
   public void LireDonnees()
   {
      Console.WriteLine("Recherche dans un fichier");
   }
}

Et:

class ClasseBase : IClasse
{
   public void LireDonnees()
   {
      Console.WriteLine("Dans une base de données");
   }
}

Et on termine avec:

class MonFramework
{
   public MonFramework(IClasse classe)
   {
      Classe = classe;
   }
   public IClasse Classe;
}

Nous voilà prêt!

La technique générale est la suivante:

  1. Création d’un conteneur Unity
  2. Enregistrement des types
  3. Instanciation

 

// création du conteneur
UnityContainer conteneur = new UnityContainer();
// enregistre les types
conteneur.RegisterType<IClasse, ClasseFichier>();

On peut alors tester:


IClasse maClasse = conteneur.Resolve<IClasse>();
maClasse.LireDonnees();

 

On constate qu’une classe est instanciée et que c’est une instance de ClasseFichier:

2015-09-04_22-03-20

 

Il est possible de résoudre IClasse de façon indirecte:


MonFramework monFramework = conteneur.Resolve<MonFramework>();
monFramework.Classe.LireDonnees();

 

Si vous lancez ce code vous verrez s’afficher la même chose que dans l’exemple qui précède.

Dans cet exemple Unity détecte que le constructeur attend une instance de IClasse. Il sait par ailleurs qu’il doit résoudre cette interface en ClasseFichier.

 

Il est également possible d’enregistrer un type dans Unity sans mappage avec une interface avec la méthode RegisterInstance.


conteneur.RegisterInstance<IGame>(new TrivialPursuit(8));

L’enregistrement d’une instance est souvent utile si vous vous intégrez dans un existant qui fournit déjà un service de singleton pour ce type, mais vous avez tout de même besoin qu’Unity puisse retrouver le type pour ses résolutions.

Cas des constructeurs paramétrés

Il peut se produire que le constructeur de la classe résolue par Unity expose des paramètres. Comment les alimenter?

La première possibilité est de fournir des valeurs par défaut. Par exemple réécrivons ClasseFichier ainsi:

public ClasseFichier(string formatData)
{
}

Si l’on souhaite proposer « Json » comme valeur par défaut de formatData:


InjectionProperty injectionProperty = new InjectionProperty("formatData", "Json");
conteneur.RegisterType<IClasse, ClasseFichier>(injectionProperty);
MonFramework monFramework = conteneur.Resolve<MonFramework>();
monFramework.Classe.LireDonnees();

 

Bien sûr il est possible de surcharger cette valeur par défaut au moment de la résolution, voire de la fournir s’il n’y a pas de valeur par défaut ce que nous allons voir maintenant.

Il est assez fréquent que votre classe expose plusieurs constructeurs. Pour sélectionner le constructeur d’injection Unity se base sur différents critères comme le nombre de paramètres.

Examinons cet exemple:

class MonFramework
{
   public MonFramework(IClasse classe)
   {
      Classe = classe;
   }

   public MonFramework(string formatData)
   {
   }

   public IClasse Classe
   {
      get; set;
   }
}

La classe dispose de deux constructeurs avec un seul paramètre. La résolution de cette classe avec Unity lève ResolutionFailedException. Le problème se résout en désignant explicitement le constructeur à utiliser à l’aide de InjectionConstructorAttribute.


[InjectionConstructor()]
public MonFramework(IClasse classe)
{
   Classe = classe;
}

L’algorithme utilisé par Unity lorsqu’il rencontre plusieurs constructeur sera donc le suivant:

  • Sélectionner le constructeur décoré de InjectionConstructorAttribute
  • Autrement sélectionner le constructeur avec le plus grand nombre de paramètres
  • Si plusieurs constructeurs répondent au critère: lever une exception

 

On peut avoir besoin de surcharger une valeur par défaut d’un constructeur, voire la classe qui sera résolue par Unity. Est-ce possible?

Resolve() expose une signature qui attend une instance de ParameterOverride, une instance ou une série d’instance.

Essayons de surcharger le IClasse résolu:


ParameterOverride paramClasse = new ParameterOverride("classe", new ClasseBase());
MonFramework monFramework = conteneur.Resolve<MonFramework>(paramClasse);
monFramework.Classe.LireDonnees();

Cette fois il s’affiche:

2015-09-05_22-06-57

 

On peut se servir de cette technique même sans avoir de résolution par défaut pour IClasse. On profite simplement de la capacité d’Unity à résoudre d’autres références qui pourraient exister dans le constructeur ou dans la classe elle même comme nous le verrons plus loin.

Le paramètre n’a pas besoin d’être spécialement d’un type enregistré dans le conteneur. Le constructeur de MonFramework pourrait très bien ressembler à ceci:

class MonFramework
{
   public MonFramework(IClasse classe, string formatData)
   {
      Classe = classe;
      FormatData = formatData;
   }
   public string FormatData { get; set; }
   public IClasse Classe;
}

Et on pourrait alors fournir une valeur particulière à formatData:


ParameterOverrides overrides = new ParameterOverrides()
{
   { "formatData","xml" },
   {"classe", new ClasseBase() }
};
 
MonFramework monFramework = conteneur.Resolve<MonFramework>(overrides);
monFramework.Classe.LireDonnees();

 

Enregistrement de plusieurs schémas pour un même type

On peut avoir besoin de résoudre un type dans telle ou telle instance d’une clase qui dépend d’une certaine valeur d’un paramètre du constructeur. Par exemple:

public class TrivialPursuit : IGame
{
   public TrivialPursuit(int nbJoueurs)
   {
   }
}
 
public class TicTacToe : IGame
{
 
   public TicTacToe(int nbJoueurs)
   {
   }
}

 

Le constructeur de ces deux classes attend une valeur entière qui va dépendre du type de jeu. Le jeu n’est connu qu’au moment de l’exécution. Comment faire?

Il est possible de donner un nom à un schéma de résolution de la façon suivante:


InjectionConstructor injectionConstructor = new InjectionConstructor(2);
conteneur.RegisterType<IGame, TrivialPursuit>("trivialPoursuit", injectionConstructor);

injectionConstructor = new InjectionConstructor(8);
conteneur.RegisterType<IGame, TicTacToe>("ticTacToe", injectionConstructor);

 

Il ne reste plus qu’à implémenter une logique de sélection:


IGame jeu = conteneur.Resolve<IGame>("ticTacToe");

 

Il existe une alternative à cette solution avec les conteneurs enfants.


UnityContainer conteneur = new UnityContainer();

InjectionConstructor injectionConstructor = new InjectionConstructor(2);
conteneur.RegisterType<IGame, TrivialPursuit>("trivialPoursuit", injectionConstructor);

IUnityContainer childContainer = conteneur.CreateChildContainer();
injectionConstructor = new InjectionConstructor(8);
childContainer.RegisterType<IGame, TicTacToe>("ticTacToe", injectionConstructor);

IGame jeu = childContainer.Resolve<IGame>("ticTacToe");

 

Comme on le voit, si le conteneur enfant ne trouve pas son type, la recherche remonte au conteneur parent. Les conteneurs enfants sont une façon pratique d’organiser des enregistrements en fonction d’un contexte, en ayant la possibilité de factoriser les enregistrements communs.

Injection par les propriétés

Nous avons déjà vu l’injection de dépendance via les constructeurs et les possibilités de surcharger les comportements ou gérer les autres propriétés attendues par les constructeurs. Voyons l’injection via les propriétés.

 

On a déjà levé un coin du voile avec InjectionProperty. Mais une autre possibilité existe.

On aurait pu écrire MonFramework ainsi:


conteneur.RegisterType<IClasse, ClasseFichier>();
class MonFramework
{
   [Dependency()]
   public IClasse Classe
   {
      get; set;
   }
}

 

On remarque la présence de l’attribut DependencyAttribute sur la propriété Classe. Vous devinez facilement l’effet qu’apporte cet attribut, ce qui peut se tester avec le code suivant:


MonFramework monFramework = conteneur.Resolve<MonFramework>();
monFramework.Classe.LireDonnees();

 

Bien sûr rien ne nous empêche de renseigner directement la propriété de façon classique:


MonFramework monFramework = new MonFramework();
monFramework.Classe = conteneur.Resolve<IClasse>();

 

On évite ainsi une dépendance de notre classe avec un framework technique.

Cycle de vie des instances

La résolution des types est une opération très rapide, le vrai coût se trouve dans l’enregistrement et de fait on devra éviter de le refaire plusieurs fois. Parmi les bonnes pratique il est recommandé de centraliser l’enregistrement des types et la construction du conteneur dans une classe appelée généralement Bootstrap. On facilite ainsi le contrôle du nombre de fois où l’on enregistre les types et évite de les enregistrer plusieurs fois.

Nous verrons des exemples concrets dans le dernier chapitre.

Une autre raison pour laquelle une expérience avec un injecteur de dépendance peut être décevante est liée à la mauvaise gestion du cycle de vie des instances.

Nous avons pour l’instant utilisé les paramétrages par défaut d’Unity: à chaque fois que l’on appelle Resolve, une nouvelle instance est créée. Il est toutefois possible de contrôler la façon dont l’instance est créée et en particulier produire des singletons.

On résout ce problème en fournissant au conteneur un gestionnaire de durée de vie, c’est à dire une classe qui hérite de la classe abstraite LifetimeManager.

Unity est livré avec plusieurs implémentations, mais vous avez bien entendu le droit de créer la votre. Voici les plus utilisés:

  1. TransientLifetimeManager
    C’est le gestionnaire par défaut lorsque l’on ne précise rien. Une nouvelle instance est créée pour chaque appel à Resolve.
  2. ContainerControlledLifetimeManager
    C’est le gestionnaire que l’on utilisera pour créer un singleton puisqu’il retourne toujours la même instance. C’est le gestionnaire utilisé par défaut lorsque l’on appelle RegisterInstance.
  3. ExternalyControlledLifetimeManager
    Unity ne créée pas une référence forte, mais une référence faible. Cela signifie que l’instance peut être nettoyée par le ramasse-miette si aucune autre référence forte de cette classe n’existe dans le code.
    Un singleton sera là aussi géré.
  4. PerThreadLifetimeManager
    Une instance par thread sera créée par Unity. C’est à dire un singleton par thread.

 

Le code peut ensuite ressembler à ça:


conteneur.RegisterType<IDemoUnity, DemoUnity>(new ContainerControlledLifetimeManager());

 

Utilisation d’une classe de démarrage

La résolution des types est une opération très rapide, le vrai coût se trouve dans l’enregistrement et de fait on devra éviter de le refaire plusieurs fois. Il existe des patterns pour organiser au mieux le code, c’est le point que nous allons aborder.

Commençons par le cas d’une application Web. Une solution est de construire une classe statique souvent appelée Bootstrap ou tout nom similaire chargée d’instancier le conteneur Unity et y enregistrer les types.

D’une façon générale on trouve des Nuget qui proposent des implémentations standards de classe de démarrage.

 

Note: Le problème de ces Nuget est qu’ils sont fait par des gens pleins de bonne volonté, mais pas vraiment maintenus, ni même bien packagés.

Le code en question étant guère important, j’ai presque envi de vous conseiller de l’intégrer directement dans votre code plutôt que de trainer des dépendances de code sans grande valeur ajoutée pour votre projet.

 

Regardons un exemple pour voir quelques unes des techniques que l’on peut mettre en œuvre. Le Nuget s’appelle Unity.WebForms. Cette librairie n’est compatible qu’avec une vieille version d’Unity, je conseille vraiment d’intégrer son code si vous la choisissez.

[assembly: WebActivator.PostApplicationStartMethod( typeof(MonNamespace.Bootstrap), "PostStart" )]
namespace MonNamespace
{
/// <summary>
///Classe de démarrage Unity
/// </summary>
internal static class Bootstrap
{
   #region PostStart
   /// <summary>
   /// Initialise le conteneur Unity au démarrage de l'application
   /// </summary>
   /// <remarks>
   ///On n'a aucune raison d'éditer cette méthode
   /// </remarks>
   internal static void PostStart()
   {
      IUnityContainer container = new UnityContainer();
      HttpContext.Current.Application.SetContainer(container);
 
      RegisterDependencies(container);
   }
   #endregion
 
   #region RegisterDependencies
   /// <summary>
   ///Enregistre les dépendances dans le conteneur fournit
   /// </summary>
   /// <param name="container>Instance du conteneur Unity à paramétrer</param>
   private static void RegisterDependencies(IUnityContainer container)
   {
      // container.RegisterType<IMoninterface, MaClasse>();
      // ici on met son code comme dans l'exemple fournit
   }
   #endregion
}
}

 

La classe est amorcée par un activateur, technique que j’ai en partie évoqué dans cet article. La classe sera activée avant même que Application_Start ne soit invoquée.

La méthode PostStart est la méthode invoquée. Cette méthode est juste une amorce, vous n’avez pas de raisons de la modifier. Elle se termine par un appel à RegisterDependencies qui est la méthode que vous allez personnaliser en fonction des besoins de votre application. C’est là que vous enregistrez les dépendances dont vous avez besoin.

Notez un point important, la ligne 19 qui est importante car c’est ici que l’on va rendre accessible le conteneur dans toute l’application. SetContainer est une méthode d’extension fournie par Unity, elle attache l’instance du conteneur à Application.

On dispose de la méthode similaire GetContainer qui récupère le conteneur:


IUnityContainer container = HttpContext.Current.Application.GetContainer();

 

Applications MVC

Voyons une autre approche qui exploite les particularités du framwork MVC.

Depuis sa version 3, MVC intègre un service de résolution de dépendance ce qui simplifie considérable l’injection dans les contrôleurs ou les filtres.

 

La clef de cette plomberie sont IDependencyResolver et DependencyResolverDependencyResolver reçoit une instance de IDependencyResolver, une classe qui connaît les dépendances dont le projet a besoin. Cette classe sera par la suite utilisée par le Framework MVC.

 

Nous fournissons une implémentation personnalisée (MyUnityDependencyResolver) comme dans l’exemple qui suit:

namespace MonNamespace
{
   public static class Bootstrapper
   {
      #region Initialize
      public static void Initialize()
      {
         IUnityContainer container = BuildUnityContainer();
         DependencyResolver.SetResolver(new MyUnityDependencyResolver(container));
      }
      #endregion
 
      #region BuildUnityContainer (private)
      private static IUnityContainer BuildUnityContainer()
      {
         IUnityContainer container = new UnityContainer();
 
         // container.RegisterType<IMoninterface, MaClasse>();
 
         return container;
      }
      #endregion
   }
}

On initialise le bootstrap en ajoutant cette ligne dans Global.asax:


Bootstrapper.Initialize();

Initialize est invoquée pour initialiser l’injection de dépendance dans le projet.

 

Note: j’ai trouvé de nombreux exemples dans lesquels on alimente DependencyResolver.Current. Je ne sais pas comment un tel code peut fonctionner car la propriété n’a qu’un getter.

Et bien sûr:

public sealed class MyUnityDependencyResolver : IDependencyResolver
{
   public MyUnityDependencyResolver(IUnityContainer conteneur)
   {
      _conteneur = conteneur;
   }
   IUnityContainer _conteneur;
 
   public object GetService(Type serviceType)
   {
      try
      {
         return _conteneur.Resolve(serviceType);
      }
      catch
      {
         return null;
      }
   }
 
   public IEnumerable<object> GetServices(Type serviceType)
   {
      try
      {
         return _conteneur.ResolveAll(serviceType);
      }
      catch
      {
         return new List<object>();
      }
   }
}

 

On pourrait vouloir exploiter directement IDependencyResolver et ses méthodes. Ce n’est pas la bonne pratique. Cette classe est dédiés aux besoins du Framework, pas à ceux de votre application.

Note: pourquoi ce n’est pas une bonne pratique?

Pour une raison qui peut apparaître discutable. DependencyResolver est un Service Locator que beaucoup de développeurs considèrent comme un anti-pattern. Même si je ne suis pas fan de ce pattern, je suis tout de même plus nuancé car dans le contexte il me semble correctement appliqué. J’ai parlé plus en détail de ce problème dans un autre article.

 

Voici une façon standard d’exploiter l’injection de dépendance dans le contrôleur HomeController:

public HomeController(ITestClasse testClasse)
{
   _testClasse = testClasse;
}

private readonly ITestClasse _testClasse;

 

Web API

Web Api dispose aussi d’un mécanisme intégré d’injection de dépendances… mais ce n’est pas le même que pour MVC! Il est toutefois très proche et nous allons reprendre un code très similaire au précédent.

 

Note: la version suivante d’ASP.NET va fusionner les deux modèles en un ce qui est une bonne nouvelle. MVC 6 va même plus loin et intègre un middleware OWIN pour gérer directement l’injection de dépendance.

 

Je vais m’inspirer de cet exemple:

http://www.asp.net/web-api/overview/advanced/dependency-injection

 

IDependencyResolver de Web Api expose également un mécanisme capable de fournir une portée afin de nettoyer automatiquement les instances résolues lors d’une requête. Ce mécanisme est introduit par IDependencyScope et IDisposable qui décorent IDependencyResolver.

Deux nouvelles méthodes vont donc apparaître dans la classe MyUnityDependencyResolver qui précède:

public IDependencyScope BeginScope()
{
   var child = _conteneur.CreateChildContainer();
   return new MyUnityDependencyResolver(child);
}
 
public void Dispose()
{
   _conteneur.Dispose();
}

BeginScope renvoit un service de résolution de dépendances à partir d’un conteneur enfant du conteneur principal. Dispose… dispose l’instance à la fin de la requête.

On n’a pas besoin de se soucier de les appeler, le Framework Web Api le fait pour nous lors de chaque requête.

 

L’instance de IDependencyScope créée par BeginScope, peut être récupérée dans le contrôleur avec :

Request.GetDependencyScope()

 

Toutes les ressources crées via les méthodes GetService de cette instance seront automatiquement disposées en appelant la méthode Dispose du scope enfant. A vous d’ajouter le code de nettoyage dans cette méthode.

L’étape suivante consiste à créer une classe Bootstrapper identique à la précédente, à l’exception de la méthode Initialize:


public static void Initialise()
{
   IUnityContainer container = BuildUnityContainer();
   GlobalConfiguration.Configuration.DependencyResolver = new MyUnityDependencyResolver(container);
}

 

On trouvera dans le répertoire App_Start une classe WebApiConfig, on ajoutera au début de Register cette ligne:


Bootstrapper.Initialise();

 

WebApiConfig est injectée dans la configuration globale en général dans Global.asax:


GlobalConfiguration.Configure(WebApiConfig.Register);

Le reste est identique à ce qui a été vu dans le chapitre qui précède.

 

Bibliographie

On trouve des exemples de code ici:

https://unity.codeplex.com/wikipage?title=Code%20samples&referringTitle=Home

Un bouquin Microsoft:

http://www.microsoft.com/en-us/download/details.aspx?id=39944

 

 

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s