mardi 1 avril 2008

Singleton générique

Tout d'abord, c'est quoi un Singleton ? Pour résumer très vite (Google est ton ami si tu veux plus de détails), un singleton est une instance unique d'une classe qui sera utilisée tout au long d'un processus. Le but de cette démarche est surtout pour moi de gagner en rapidité lors de l'écriture et aussi lors de l'exécution.

Imaginons que j'ai une classe MyClass et à l'intérieur une méthode DoThings. Si je veux appeler cette méthode je suis obligé de faire :

   1: MyClass myClass = new MyClass(); 
   2: myClass.DoThings();

et cela chaque fois que je voudrai appeler la méthode. C'est une perte de temps dans l'écriture du code lui-même mais aussi une perte de temps dans l'exécution puisqu'à chaque fois il devra instancier une nouvelle fois la classe (et tout ce que cela implique niveau mémoire etc). Dans ce cas-là, on pourrait se dire qu'il suffirait de mettre DoThings en méthode statique. De manière générale je suis un peu contre mettre les choses en static sauf si l'utilité est réellement prouvée. Dans notre cas, c'est seulement parce que l'on est feignant, donc je m'y refuse.

La méthode la plus simple, et la plus répandue aussi je crois, pour obtenir le singleton serait de définir des méthodes statiques dans la classe pour effectuer cela.

   1: public class MyClass
   2: {
   3:     private static MyClass _singleton;
   4:  
   5:     static MyClass()
   6:     {
   7:         _singleton = new MyClass();
   8:     }
   9:  
  10:     public static MyClass GetInstance()
  11:     {
  12:         return _singleton;
  13:     }
  14:  
  15:     public void DoThings()
  16:     {
  17:     }
  18: }
Le constructeur statique instancie une fois la classe et la stocke dans une variable statique elle aussi. A chaque appel de la méthode statique GetInstance, on retourne toujours la même instance. Ainsi là où l'on avait les deux lignes définies plus haut, on a plus que ceci :
   1: MyClass.GetInstance().DoThings();

Il me semble que c'est déjà pas mal non ? Et en plus ca marche très bien. Le seul petit problème, c'est que l'on doit se taper la variable, le constructeur et la méthode dans chaque classe où l'on souhaite avoir le singleton. La solution ? Les génériques !

Grâce aux génériques on va pouvoir se créer des singletons une bonne fois pour toutes, et à un seul endroit en plus. Sans plus attendre, le code :

   1: public static class Singleton<T> where T : class, new()
   2: {
   3:     private static T _singleton;
   4:  
   5:     static Singleton()
   6:     {
   7:         _singleton = new T();
   8:     }
   9:  
  10:     public static T GetInstance()
  11:     {
  12:         return _singleton;
  13:     }
  14: }

Le code indique que la classe prendra un argument en générique qui, grâce à la clause where, devra être une classe, et avoir un constructeur public par défaut. Le reste du code est identique au code présenté précédemment. L'appel quant à lui change un peu, il faut désormais appeler la méthode DoThings de cette manière :

   1: Singleton<MyClass>.GetInstance().DoThings();

Je le concède, cela prends plus de caractères pour faire l'appel, cependant cela devient beaucoup plus simple lors de l'écriture des classes. En plus on peut le customiser pour posséder plusieurs singleton d'une même classe. Par exemple, imaginons un système qui impose d'aller taper une base de données dans un cas, et une autre base de données dans un autre ; il nous faudrait deux singletons, un pour chaque base de données.

Pour cela on peut définir une seconde classe Singleton qui permettrait d'obtenir un singleton en fonction d'un paramètre fourni lors de l'appel à GetInstance. Sans plus attendre, le code qui permet ça :

   1: public static class Singleton<T, P> where T : class, new()
   2: {
   3:     private static Dictionary<P, T> _singleton;
   4:     private static ReaderWriterLockSlim _lock;
   5:  
   6:     static Singleton()
   7:     {
   8:         _singleton = new Dictionary<P, T>();
   9:         _lock = new ReaderWriterLockSlim();
  10:     }
  11:  
  12:     public static T GetInstance(P param)
  13:     {
  14:         T t;
  15:         _lock.EnterUpgradeableReadLock();
  16:         try
  17:         {
  18:             if (!_singleton.TryGetValue(param, out t))
  19:             {
  20:                 _lock.EnterWriteLock();
  21:                 try
  22:                 {
  23:                     if (!_singleton.ContainsKey(param))
  24:                     {
  25:                         t = new T();
  26:                         _singleton.Add(param, t);
  27:                     }
  28:                 }
  29:                 finally
  30:                 {
  31:                     _lock.ExitWriteLock();
  32:                 }
  33:             }
  34:         }
  35:         finally
  36:         {
  37:             _lock.ExitUpgradeableReadLock();
  38:         }
  39:         return t;
  40:     }
  41: }

Le code ce coup-ci a pas mal changé. Tout d'abord on voit apparaître un dictionnaire qui va contenir tous nos singletons en fonction d'une clé qui sera le paramètre fourni lors de l'appel à GetInstance. On reconnait aussi le ReaderWriterLockSlim qui est là pour éviter les collisions de threads lorsque deux threads concurrents essaieront d'obtenir un même singleton qui n'existe pas encore.

Normalement le code devrait être Thread-Safe, mais j'avoue que je n'ai fait aucune charge pour m'en assurer.

Pour obtenir un singleton paramétré, on fait désormais cet appel :

   1: Singleton<MyClass, int>.GetInstance(1).DoThings(); 
   2: Singleton<MyClass, int>.GetInstance(2).DoThings();

Ces deux lignes utiliseront deux instances distinctes de la classe MyClass.

Aucun commentaire: