Design Patterns : Singleton

Voici le premier article d’une série consacrée à la compréhension et à l’utilisation des Design Patterns.

Au départ la compréhension et l’utilisation des design patterns peut sembler inutile, pourtant ils répondent à des problématiques que l’on retrouve dans tout les projets informatiques. Ces design patterns ont tous montré leur efficacité depuis le temps qu’ils sont utilisés. On a tous, sans forcément le savoir, utilisé un de ces design patterns au cours de nos vies professionnelles.

Au fil des articles présentant les design patterns j’essaierai autant que possible d’y associer des exemples concrets pour faciliter la compréhension.

Je vais commencer par l’un des design pattern les plus simple…le Design Pattern Singleton

Contexte

Une grande entreprise Française nous demande de réaliser un logiciel de contrôle de réacteur nucléaire, et là autant dire que l’erreur est interdite. Le logiciel devra gérer l’ajout des barres d’uranium, la mise en route du réacteur ainsi que son arrêt. De plus deux salles des commandes partageront le contrôle du réacteur.

Proposition 1

Facile comme tout me direz-vous, une classe réacteur avec les méthodes qui conviennent et le tour est joué ! Je crée un constructeur qui va initialiser mon réacteur avec 0 kg d’uranium, une autre pour savoir si le réacteur est démarré ou non. Voici ma classe Réacteur :

  public class Reacteur
    {
        private int _uraniumCharge;
        private bool _estReacteurDemarre;

        public Reacteur()
        {
            this._uraniumCharge = 0;
            this._estReacteurDemarre = false;
        }

        public void ChargerReacteur()
        {
            if (!this._estReacteurDemarre)
            {
                this._uraniumCharge += 50;
                Console.WriteLine("Le réacteur contient {0} kg d'Uranium", this._uraniumCharge);
            }
        }

        public void DemarrerReacteur()
        {
            if (this._uraniumCharge == 50 && !this._estReacteurDemarre)
            {
                this._estReacteurDemarre = true;
                Console.WriteLine("Le réacteur est démarré...");
            }
        }

        public void ArreterReacteur()
        {
            if (this._estReacteurDemarre)
            {
                this._estReacteurDemarre = false;
                this._uraniumCharge = 0;
                Console.WriteLine("Le réacteur est arrêté...");
            }
        }
    }

Ok mais je vois quand même comme un problème. J’ai deux salles de commandes qui partagent la gestion du même réacteur…si chacune d’elle crée une instance de réacteur je vais me retrouver à charger deux fois le même réacteur…ouch !

Ma classe est bonne pourtant, seulement il me faut m’assurer qu’une et une seule instance de réacteur puisse être créée…c’est le principe du design pattern singleton !

Proposition 2

On en vient donc à une deuxième proposition, cette fois-ci en implémentant le pattern singleton, comment ?

Déjà il ne faut pas que la classe Réacteur puisse être instanciée depuis l’extérieur, passer le constructeur en privé fera l’affaire. Par contre je me retrouve avec une classe non instanciable ! Pour régler ce souci rien de plus simple, je crée une variable statique privée de type Reacteur dans ma classe Reacteur associée à une méthode statique d’obtention de ma variable !…rien ne vaut un peu de code pour comprendre !

 public class Reacteur
    {
        // Ma variable de type Réacteur
        private static Reacteur _instance;

        private int _uraniumCharge;
        private bool _estReacteurDemarre;

        private Reacteur()
        {
            this._uraniumCharge = 0;
            this._estReacteurDemarre = false;
        }

        /// <summary>
        /// Méthode d'obtention de mon instance de réacteur
        /// </summary>
        /// <returns></returns>
        public static Reacteur GetReacteur()
        {
            // Si mon réacteur n'est pas instancié je crée une nouvelle instance, le constructeur
            // étant privé seule un appel depuis une méthode interne peut fonctionner.
            if (_instance == null)
                _instance = new Reacteur();

            return _instance;
        }

        // ChargerReacteur, DemarrerReacteur et ArreterReacteur restent les mêmes

Voilà, écrivons maintenant un bout de code pour tester :

class Program
    {
        static void Main(string[] args)
        {
            Reacteur posteCommande1 = Reacteur.GetReacteur();
            Reacteur posteCommande2 = Reacteur.GetReacteur();

            // Le poste de commande 1 charge et lance le réacteur
            posteCommande1.ChargerReacteur();
            posteCommande1.DemarrerReacteur();

            // Le poste de commande 2 essaye de charger et d'arrêter le réacteur
            // Le chargement ne peut se faire vu que le réacteur est déjà chargé ! ouf !
            posteCommande2.ChargerReacteur();
            posteCommande2.ArreterReacteur();

            Console.ReadLine();
        }
    }

Voici le résultat :

Le réacteur contient 50 kg d’Uranium
Le réacteur est démarré…
Le réacteur est arrêté…

Il est l’heure de sabrer le champagne, tout fonctionne sans aucun danger, on est persuadé qu’il ne sera plus possible de charger sans le faire exprès deux fois le réacteur ! quel soulagement. Quoiqu’il arrive c’est la même instance de Reacteur qui est utilisée.

Et là…c’est le drame

Un matin, alors que le logiciel est déjà en production depuis des mois et qu’il donne entière satisfaction au client nous recevons un coup de fil affolé de la centrale…aie aie aie le logiciel a tenté de charger deux fois le réacteur, heureusement que l’opérateur a stoppé le processus au dernier moment sinon c’était l’accident assuré ! Mais pourquoi….

En retraçant les historiques on se rend compte que les opérateurs de la salle de commande 1 et de la salle de commande 2 ont lancés le logiciel et ordonnés en même temps le chargement du réacteur. Deux threads donc qui arrivent au même moment sur la méthode GetReacteur(), chacun crée une instance de Reacteur et charge donc deux fois le réacteur…ouch ! Voici le code de test :

 static void Main(string[] args)
        {
            Thread t1 = new Thread(() => Reacteur.GetReacteur().ChargerReacteur());
            Thread t2 = new Thread(() => Reacteur.GetReacteur().ChargerReacteur());

            t1.Start();
            t2.Start();
        }

Attention suivant comment sont exécutés les deux threads les résultats peuvent varier mais voici l’effet recherché :

Le réacteur contient 50 kg d’Uranium
Le réacteur contient 50 kg d’Uranium

Deux solutions ici, soit appliquer un lock sur la création de l’instance de réacteur comme ceci :

 // Ma variable de type Réacteur
        private static Reacteur _instance;
        private static Object _lock = new object();

        private int _uraniumCharge;
        private bool _estReacteurDemarre;

        private Reacteur()
        {
            this._uraniumCharge = 0;
            this._estReacteurDemarre = false;
        }

        /// <summary>
        /// Méthode d'obtention de mon instance de réacteur
        /// </summary>
        /// <returns></returns>
        public static Reacteur GetReacteur()
        {
            lock (_lock)
            {
                if (_instance == null)
                    _instance = new Reacteur();
            }
            return _instance;
        }

        // ChargerReacteur, DemarrerReacteur et ArreterReacteur restent les mêmes

Le problème est qu’à chaque appel à GetReacteur on fait appel au processus de lock qui peut-être gourmand…un système de double check de la valeur Null sur _instance pourrait fonctionner également pour éviter d’avoir à locker un objet à chaque fois. Enfin ceci n’est pas très beau niveau code, personnellement je serais plus pour déclarer la variable _instance comme static readonly, elle sera instanciée de façon thread safe quoiqu’il arrive, de plus cela permet d’enlever du code dans la méthode GetReacteur(). Au final voici ce que l’on obtient :

 public class Reacteur
    {
        // Ma variable de type Réacteur
        private static readonly Reacteur _instance = new Reacteur();

        private int _uraniumCharge;
        private bool _estReacteurDemarre;

        private Reacteur()
        {
            this._uraniumCharge = 0;
            this._estReacteurDemarre = false;
        }

        /// <summary>
        /// Méthode d'obtention de mon instance de réacteur
        /// </summary>
        /// <returns></returns>
        public static Reacteur GetReacteur()
        {
            return _instance;
        }

        // ChargerReacteur, DemarrerReacteur et ArreterReacteur restent les mêmes

Voilà ! Notre système est désormais super sécure !

Définition du Singleton :

Ensure a class has only one instance and provide a global point of access to it.

Diagramme de classe :

Singleton

Leave a Reply