Tout d’abord, qu’est-ce qu’un Singleton ?

Un Singleton est un Design Pattern ou « patron de conception », qui permet de s’assurer qu’une classe ne possède qu’une seule instance, et ne propose qu’un seul et unique point d’accès à celle-ci. C’est d’ailleurs l’un des plus simples à comprendre et de fait, l’un des plus connus.

Je vous invite d’ailleurs à lire l’article dédié aux Design Patterns sur ce blog.

Le but de cette implémentation est d’obtenir un objet qu’on pourrait qualifié d’unique dans le sens où son instanciation sera singulière. Ainsi, sa seule et unique instance sera forcément partagée, et ne pourra pas être dupliquée.

Version historique

public class Singleton
{
    private static Singleton instance;

    private Singleton()
    {
    }

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

Cette version doit être la plus communes de toute. Elle est simple, correcte et respecte les principes du pattern Singleton.

On peut y remarquer :

  • Le champ de l’instance, privé, donc non accessible depuis l’extérieur de la classe. Et statique, s’assurant du maintien de l’instance crée.
  • Le constructeur de la classe, privé lui aussi, donc non accessible depuis l’extérieur.
  • La propriété publique qui donne accès à la variable d’instance, en la créant si elle n’existe pas au préalable.

Malheureusement, dans le cas du multi-threading, cette conception peut ne pas garantir à 100% l’unicité de l’instance créée. En effet, si deux threads atteignent en même temps l’expression conditionnelle if (instance == null), il se peut qu’ils obtiennent la même réponse positive et ainsi, allouent deux fois de suite un nouvel objet Singleton à la variable d’instance.

Version statique

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

    private Singleton()
    {
    }

    public static Singleton Instance
    {
        get 
        {
            return instance;
        }
    }
}

Dans cette version, on corrige l’implémentation précédente dite « non thread-safe » par l’ajout d’une initialisation statique. Et par l’utilisation du mot-clé readonly qui va assurer qu’en dehors d’un éventuel constructeur ou de l’initialisation elle-même, la variable d’instance ne pourra être modifiée.

L’initialisation statique sera gérée par le CLR (Common Language Runtime) lorsqu’il chargera la classe. Donc dans ce cas, on peut être sûr que même dans un environnement multi-threadé, l’initialisation se fera une seule et unique fois.

Parfait, me direz-vous. Malheureusement, cette solution ne respecte pas vraiment les principes du pattern Singleton car elle ne laisse pas un contrôle optimal sur la création de l’objet en lui-même. En effet, étant donné que c’est le framework .NET qui s’occupera de l’instantiation, l’appel à un constructeur autre que celui par défaut, sera impossible. Cette approche n’en reste pas moins l’une des plus utilisées de par sa simplicité.

Version du lock

public class Singleton
{
    private static Singleton instance;
    private static readonly object locker = new object();

    private Singleton()
    {
    }

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

Cette version s’approche de la perfection en introduisant l’utilisation du mot-clé lock, qui est un alias de l’utilisation de la classe Monitor.

On peut y distinguer :

  • Le champ de l’objet qui va servir de verrou, privé, statique et en lecture seule.
  • Le mot-clé lock qui va verrouiller l’objet préalablement initialisé, et bloquera en attente tout Thread qui tentera de rentrer dans le bloc d’instructions marqué comme section critique, en même temps qu’un autre.

Le seul inconvénient de cette conception, est que l’utilisation du lock est très coûteuse. Seulement, ici elle sera systématique.

Version du double check

public class Singleton
{
    private static Singleton instance;
    private static readonly object locker = new object();

    private Singleton()
    {
    }

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

Cette approche est quasi parfaite. Elle est respectueuse des principes du pattern Singleton tout en étant thread-safe. De plus, le coût de l’utilisation du lock est contrôlé par la même expression conditionnelle que pour l’initialisation du Singleton lui-même. En d’autres termes, si l’instance existe déjà, inutile de faire appel au mécanisme de verrou.

Bonus : Version finale

public sealed class Singleton
{
    private static volatile Singleton instance;
    private static readonly object locker = new object();

    private Singleton()
    {
        if (instance != null)
            throw new Exception("Cannot using reflection");
    }

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

Voici l’approche la plus représentative du Singleton car non seulement elle respecte la définition même de ce pattern, mais elle est aussi thread-safe et non héritable grâce à l’utilisation du mot-clé sealed.

Ensuite, sa création explicite est strictement impossible, même via l’utilisation de la réflexion car un deuxième appel à son constructeur lèvera une exception.

Enfin, on introduit le mot-clé volatile à la déclaration de la variable d’instance. Celui-ci va assurer la mise à jour de la valeur la plus récente au champ instance, en désactivant les optimisations du compilateur qui supposent un accès mono-threadé. En soit, ce mot-clé qui suppose une utilisation multi-thread pourrait quasiment permettre la suppression du double check.

Pour conclure

Force est de constater qu’un Design Pattern, même simple, peut avoir plusieurs implémentations différentes. Ceci est dû aux nombreuses interprétations personnelles et à la volonté de chacun d’améliorer l’approche qu’il a de la solution. De plus, le langage utilisé joue un rôle important dans ces divergences étant donné les particularités qui lui sont propres.

Personnellement, et comme beaucoup de monde, j’utilise l’approche statique du Singleton. Celle-ci me paraît être la plus simple en terme de lisibilité et de performance. Mais ce n’est que mon avis. Et vous, quelle approche utiliseriez-vous ?