Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Singleton qui es-tu ?

C’est un thème que je n’ai jamais abordé seul en près de 900 billets depuis 2008 et pourtant j’en ai parlé souvent, même très récemment. Ne serait-il pas temps de faire le point sur ce design pattern ?…

Singulier vs Pluriel

imageLe besoin de se définir comme unique, le “je”, ne concerne pas seulement les humains mais aussi ces petits acteurs autonomes qu’on appelle des objets dans notre métier…

 

Techniquement les objets ont parfois le besoin d’être uniques pour être utiles. Le pluriel, la multitude, sont souvent synonymes d’interchangeabilité. Du point de vue de l’humain qui possède une conscience se voir assimiler à une foule sans visage est la pire des négations, mais pas pour les objets ! Leur multitude est leur avantage comme les fourmis dans une fourmilière. Mais cet avantage a ses limites. Les ruches ont leur reine, les navires leur capitaine, ils ne tirent pas tant leur légitimité du pouvoir qu’ils incarnent que de leur unicité et de la constance dans le temps de leur fonction, piliers singuliers sur lesquels le pluriel peut se reposer pour s’organiser. Les applications ont elles aussi besoin de certains point d’ancrage fiables et constant dans le temps. La notion de “service”, les Factories et même les conteneurs IoC ont besoin de tels points fixes. Cette position particulière a été étudiée et porte un nom : le Singleton.

Le Singleton méritait bien une petite intro philosophico-technique !

Le Design Pattern Singleton

La référence absolue en matière de Design Patterns c’est le livre du Gang Of Four. Ca commence à dater mais c’est toujours d’une actualité indiscutable. Il est donc toujours temps de vous le procurer et de le lire et le relire ! En attendant voici la définition théorique du Singleton car rien ne peut se discuter sans l’exposé et la compréhension de celle-ci.

Classification

Le Singleton est classé dans la catégorie des Patterns dit “Creational” groupe qui contient l’Abstract Factory, le Builder, la Factory Method et le Prototype. Leur point commun est d’aider un système à être indépendant vis-à-vis de la création de ses objets, leur composition (au sens des liens entre instances) et leur représentation. Tous ces patterns offrent des abstractions du processus d’instanciation.

Intention

S’assurer qu’une classe ne possède qu’une seule instance et assurer un point d’accès unique et bien défini à celle-ci.

Motivation

j’ai déjà évoqué ici ou dans d’autres articles l’intérêt pour certains objets d’être uniques et “centraux” (accessibles de partout).

Mais on peut ajouter d’autres circonstances comme par exemple tout ce qui est relié à des ressources physiques. Un système d’impression par exemple. Il ne suffira pas de multiplier les instances en mémoire pour posséder plusieurs imprimantes au lieu d’une ! Et même si on permet de lancer plusieurs impressions en même temps, elles seront stockées dans un Spool qui lui sera unique. Au sein d’une application un système d’affichage de messages à destination de l’utilisateur aura tout intérêt à être lui aussi unique. On pourrait y passer des jour à lister toutes les situations qui réclament la présence d’une instance unique et accessible facilement.

Applicabilité

On utilise un Singleton quand :

  • Il doit y avoir une et une seule instance d’un objet précis et qu’elle doit être accessible par les clients depuis un point d’accès bien identifié.
  • L’instance unique doit pouvoir être étendue par sous-classement et que les clients doivent pouvoir utiliser la version étendue sans modifier leur code.

Si le premier point est généralement bien compris, le second est moins “médiatisé”. Il explique à lui seul pourquoi un Singleton ne s’implémente pas avec une classe statique non héritable par nature.

Structure

singl014

Pour qui sait lire UML (ici très simple) on retrouve les ingrédients des principales implémentations. Ce qui est normal puisque toutes se sont inspirées à un moment où un autre de ce schéma fondateur !

Participants

Le Singleton lui-même. Une seule classe participe à ce design pattern.

Collaborations

Les clients accèdent au Singleton uniquement au travers du point d’accès “Instance” qu’il offre. Une fois l’instance obtenue toutes les opérations offertes par le Singleton sont accessibles.

Conséquences

L’utilisation du Singleton implique les conséquences suivantes :

  • Contrôle des accès à l’instance unique. Puisque le Singleton encapsule sa seule instance et les moyens d’y accéder il peut contrôler, filtrer, logger les accès des clients.
  • Réduction de l’espace de nom. En C# le problème ne se pose pas mais dans les langages qui autorisent la déclaration de variables globales l’utilisation du Singleton permet de clarifier les choses et évite de polluer cet espace commun.
  • Raffinement des opérations et représentations. La classe du Singleton peut être sous-classée et il est facile de configurer l’application pour qu’elle utilise de façon transparente la nouvelle classe.
  • Autorise un nombre variable d’instances. Le Singleton, quoi que ce nom signifie, est un pattern qui est assez souple pour autoriser un changement d’avis quant au nombre des instances qu’il gère. Il peut donc aussi servir à contrôler un nombre fixe ou variables d’instances.
  • L’approche est plus souple que l’utilisation de membres ou classes statiques. Ne serait-ce que parce que ces dernières n’autorisent pas l’héritage.

Les implémentations

Il existe plusieurs façons d’implémenter le pattern Singleton même si au bout du compte seule une ou deux sont à retenir.

La version historique

Commençons par la version “historique” celle qui suit les recommandations du livre “Design Patterns : Elements of Reusable Object-Oriented Software” du Gang of Four paru en 1995. En C# cela donne :

using System;

public class Singleton
{
   private static Singleton instance;

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         if (instance == null)
         {
            instance = new Singleton();
         }
         return instance;
      }
   }
}

Cette version possède deux avantages :

  • Parce que l’objet est instancié dans la propriété “Instance” la classe peut à loisir faire d’autres traitements, notamment instancier une sous-classe.
  • L’instance n’est pas créée tant qu’il n’y a pas eu un appel à la propriété, procédé appelé “lazy instanciation”. L’effet est bénéfique puisque la création l’instance n’a lieu que lorsqu’elle est réellement nécessaire.

Toutefois il existe un point faible à cette implémentation, elle n’est pas thread-safe. Si plusieurs threads entrent dans la propriété Instance en même temps plusieurs instances de l’objet peuvent éventuellement être créées.

Initialisation statique

L’une des raisons invoquée à l’origine pour déconseiller l’utilisation d’une initialisation statique est que C++ est très ambigu sur l’ordre des initialisations statiques. Mais C# a des spécifications bien plus claires et l’ordre est garanti.

Il devient donc possible en C# d’utiliser une initialisation statique de l’instance ce qui donne :

public sealed class Singleton
{
   private static readonly Singleton instance = new Singleton();
   
   private Singleton(){}

   public static Singleton Instance => instance;
}

L’avantage de l’initialisation statique est qu’elle conserve la principe du lazy instanciation puisque l’instance ne sera créée que la première fois qu’un membre de la classe sera référencé. Le CLR (Common Language Runtime) gère cet aspect.

Le fait que la classe soit marquée “sealed” est un choix qui se discute forcément et n’est pas obligatoire. L’avantage est de s’assurer qu’aucune sous-classe ne pourra être créée et donc qu’aucune autre instance ne pourra être obtenue par ce biais. Si la classe contrôle une device physique c’est une bonne idée qui offrira une garantie de plus.
Mais dans d’autres circonstances le fait de celer la classe ne fait que rendre impossible le sous-classement qui plus haut était présenté comme l’un des avantages du pattern Singleton… C’est donc à chacun de trancher en fonction du contexte.

On remarquera aussi que la variable privée est marquée “readonly” cela permet de s’assurer qu’en dehors du constructeur éventuel (absent ici puisqu’inutile) le champ ne pourra pas être modifié. C’est une bonne protection. De même le constructeur est privé de telle façon qu’aucun autre code ne puisse créer une instance du Singleton.

Le point d’entrée est statique (la propriété  Instance) et permet un accès logique et centralisé. Ce qui fait partie de la définition d’un Singleton.

Ce code diffère principalement du précédent par le fait qu’il se repose sur le CLR pour instancier l’objet.

Le seul problème de cette solution est que le singleton est instancié via son constructeur par défaut. Or un Singleton se caractérise par le contrôle qu’il exerce sur la création de l’instance. Il peut par exemple appeler un constructeur autre que celui par défaut, lui passer d’éventuels paramètres, etc. L’initialisation statique n’apparait donc pas optimale de ce point de vue même si elle permet un code très court et fiable même en environnement multi-threadé.

Singleton thread-safe

L’initialisation statique est donc une bonne solution qui fonctionnera dans de nombreux cas. Toutefois dès lors qu’il faudra utiliser un constructeur autre que celui par défaut ou réaliser d’autres tâches avant l’instanciation dans un environnement multithreadé on devra se tourner vers d’autres implémentations.

Bien entendu la solution passe par l’utilisation de mécanismes de verrouillage propre au langage et à la plateforme considérée. Avec C# sous .NET ou UWP nous savons que nous pouvons nous baser par exemple sur l’opération “lock” qui utilise en interne Monitor (et donc simplifie l’écriture du code).

Or locker tous les accès à la propriétés possède un coût. On aimerait limiter ce dernier.

Ce qui a amené au développement d’un pattern appelé “double-check locking”. Il s’agit ici d’acquérir le verrou que si cela est nécessaire, donc une seule fois lorsque le Singleton n’est pas encore instancié.

Le code devient le suivant :

public sealed class Singleton
{
   private static volatile Singleton instance;
   private static object syncRoot = new Object();

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         if (instance == null) 
         {
            lock (syncRoot) 
            {
               if (instance == null) 
                  instance = new Singleton();
            }
         }

         return instance;
      }
   }
}

Le “double-check” se voit clairement puisque deux fois, hors et dans le lock, la variable instance est testée sur le nul. Double contrôle donc.

Il existe une polémique autour du double-check locking qui a surtout remué la communauté Java. Le procédé ne serait totalement fiable en raison du modèle mémoire de ce langage, ce qui peut être démontré. Et du fait que le résultat de ce pattern ne soit pas constant selon les compilateurs c’est le pattern lui-même qui est tombé en désuétude puisqu’un design pattern se veut le plus générique possible.

Or C# et son CLR corrigent bien des problèmes de C++ et de Java, et notamment en ce qui concerne le problème lié au double-check locking nous avons l’assurance qu’il ne peut se produire. L’utilisation de ce pattern dans ce contexte est donc parfaitement légitime.

Ainsi le code ci-dessus fonctionne parfaitement C#. Mais on remarquera une autre précaution : l’utilisation du mot clé “volatile” dans la déclaration de la variable.

Ce mot clé rarement utilisé et même connu par les développeurs permet de s’assurer que la variable ne sera lue qu’après son assignation (en tout cas ici). Ce qui nous permet d’être certain du bon fonctionnement de l’ensemble.

Volatile est méconnu, car comme tout ce qui tourne autour du multitâche il y a comme un halo de mystère qui rend les choses troubles, floues et inquiétantes. Pourtant volatile a un rôle à jouer dans ce type d’environnement multitâche. Pour être plus précis sur son fonctionnement disons qu’il indique qu’un champ peut être modifié par plusieurs threads en même temps. Les optimisations du compilateur sont désactivées pour la variable ainsi marquée (optimisation qui supposent une utilisation mono thread). De fait un thread lecteur sera assuré de toujours lire la valeur la plus à jour. D’un certain point de vue “volatile” permet de se passer des locks.

Le code ci-dessus pour fonctionner correctement traine ainsi avec lui la sulfureuse réputation du double-check locking en Java et les mystères afférents à l’utilisation de “volatile”. C’est donc un code qui, malgré les assurances que C# fonctionne de façon plus fiable que C++ ou Java, créée une sorte de réticence chez certains.

De fait ces personnes méfiantes préfèreront une version plus explicite du verrouillage, quitte à ce qu’elles soient moins efficace, et qui consiste tout simplement à locker totalement l’accès à la variable au niveau de la propriété, autant en lecture qu’en écriture.

Dans ce cas plutôt que de faire compliqué sans vrai raison, et à condition qu’on puisse assumer le fait que l’objet ne pourra être créé que par son constructeur par défaut (et sans traitement avant cette création) la version avec initialisation statique sera certainement la meilleure solution, fiable et ultra simple comme le prouve le code plus haut dans cet article. A noter que l’on perd aussi la lazy instanciation ce qui au final ne sera gênant que très rarement (car si le code n’est pas appelé, après tout il doit être supprimé, et s’il est appelé, plus tôt ou plus tard ne fait en réalité que peu de différence pour la variable instanciée sauf si elle implique des ressources très gourmandes).

Conclusion

Par le rôle qu’il joue dans de nombreuses applications le Singleton est un pattern essentiel à connaitre, comprendre et maitriser.

Au départ très simple, son implémentation peut se compliquer conceptuellement (le code reste court) dès lors qu’on veut gérer tous les aspects en même temps (lazy instanciation, thread-safe…).

Comme l’initialisation statique permet le code le plus court et qu’elle est thread-safe, c’est mon implémentation préférée.

Codiez-vous correctement le Singleton en parfaite connaissance de cause ? En tout cas maintenant vous savez tout pour le faire convenablement !

Stay Tuned !

blog comments powered by Disqus