Le problème du singleton

Décembre 2016

Le "singleton" est l'un des patrons de conception (design pattern) le plus connu et le plus simple à mettre en oeuvre. Seulement, c'est aussi l'un des plus mal utilisés, car il est souvent choisi pour mettre en place une mauvaise pratique : le contexte global.

En effet, avec un singleton, tel que nous le concevons la plupart du temps, il est assez simple de récupérer l'instance depuis n'importe quel point de notre application, et de créer ainsi, des dépendances transverses qui deviennent, par la suite, difficile à maintenir et à faire évoluer. En d'autres termes, avec le singleton, on peut vite se lancer dans la cuisine des spaghettis bolognaise qui tachent !

Implémentation courante d'un singleton

La plupart du temps, on s'appuie sur 2 points importants pour concevoir un singleton en PHP :
  • Une modification de la visibilité du constructeur pour interdire l'instanciation à l'extérieur de la classe (+interdiction du clonage).
  • Création d'une méthode statique permettant de fournir l'instance :


<?php
class Singleton {
   private static $instance = null;

   private function __construct()
   {
   }

   private function __clone()
   {
   }

   public static function getInstance()
   {
      if (self::$instance === null) {
         self::$instance = new self();
      }
      return self::$instance;
   }
}

$singleton = Singleton::getInstance();

// Génère une erreur fatale. Catchable avec PHP 7
// "Call to private Singleton::__construct() from invalid context"
$singleton = new Singleton();


Le problème majeur dans cette implémentation, c'est l'utilisation d'une méthode statique qui permet à n'importe quel développeur de récupérer l'instance quelque soit le contexte. Par exemple, une vue en charge de l'affichage d'une liste de choix, qui appelle directement la base de données pour lancer une requête SQL et récupérer son contenu.

Ce n'est pas qu'il faille absolument faire du MVC, mais la séparation des responsabilités permet d'obtenir un niveau de maintenabilité optimum. Avec des singletons, qui permettent de récupérer facilement un adaptateur de BDD ou des conteneurs de services, quand le développeur est pris par le temps, il va aller au plus court, et une fois la première enclenchée, il se laisse rapidement griser par la vitesse.

Quelle solution ?

En fait, le problème étant la méthode statique, il faudrait plutôt s'orienter vers une fabrique ("factory") dont le rôle est de nous retourner une instance de la classe et si on l'appelle plusieurs fois, on retourne l'instance déjà créée. On n'a donc plus une seule classe, mais deux.

Le problème avec cette solution, c'est que le constructeur devient donc publique, et qu'il est désormais possible de créer d'autres instances de notre classe de singleton.

Pour éviter cela, depuis PHP 7, il est possible de créer une classe anonyme au sein même de notre fabrique :

<?php
class Factory {
    private $instance = null;
    
    public function getInstance()
    {
        if ($this->instance === null) {
            $this->instance = new class() {
                private $var;
                public function get() {
                    return $this->var;
                }
                public function set($value) {
                    $this->var = $value;
                }
            };
        }
        return $this->instance;
    }
}

$factory = new Factory();
$o = $factory->getInstance();
$o->set(5);
$o2 = $factory->getInstance();

// Affiche "int(5)"
var_dump($o2->get());


Le problème dans cette solution, c'est qu'il n'est pas possible de faire du typage de paramètre (type hint), car la classe retournée est anonyme "object(class@anonymous)". Ceci est un faux problème dans la mesure où le typage de paramètre devrait plutôt s'appuyer sur des interfaces, et dans ce cas on proposera une classe anonyme qui implémente une interface :

<?php
interface MyInterface {
    public function get(): int;
    public function set(int $value);
}

class Factory {
    private $instance = null;
    
    public function getInstance(): MyInterface
    {
        if ($this->instance === null) {
            $this->instance = new class() implements MyInterface {
                private $var;
                public function get(): int {
                    return $this->var;
                }
                public function set(int $value) {
                    $this->var = $value;
                }
            };
        }
        return $this->instance;
    }
}

function display(MyInterface $object)
{
    var_dump($object->get());
}

$factory = new Factory();
$o = $factory->getInstance();
$o->set(5);
$o2 = $factory->getInstance();

display($o2);


Le problème maintenant, c'est qu'il est possible d'instancier plusieurs fabriques et donc de disposer de plusieurs instances de notre singleton. On pourrait déclarer la propriété "$instance" en statique, mais dans ce cas, on retombe sur le travers qui consiste à créer une instance de la fabrique à n'importe quel endroit du code pour disposer de notre singleton.

En fait, ce qu'il faudrait, c'est interdire d'instancier plusieurs fois notre classe et conserver précieusement notre unique instance dans notre fabrique : Une classe Mousquet (Musket Class). Seulement, on se heurte à un problème des classes anonymes : chaque classe est différente d'une instance à l'autre, et l'utilisation d'une propriété statique ne permet pas d'identifier si c'est une nouvelle instance
<?php

class Factory {
    private $instance = null;
    
    public function getInstance()
    {
        if ($this->instance === null) {
            $this->instance = new class() {
                private static $instanciated = false;
                
                public function __construct()
                {
                    if (self::$instanciated) {
                        throw new RuntimeException('Could not instanciate class');
                    }
                    self::$instanciated = true;
                }
                
                private $var;
                
                public function get(): int {
                    return $this->var;
                }
                
                public function set(int $value) {
                    $this->var = $value;
                }
            };
        }
        return $this->instance;
    }
}

$factory = new Factory();
$o = $factory->getInstance();
$o->set(5);

$factory2 = new Factory();
// On s'attendrait à avoir une exception ici ... mais non
$o2 = $factory->getInstance();
var_dump($o2->get());


Mais du coup, comme on ne peut pas instancier plusieurs fois notre classe, on n'est plus obligé d'utiliser une classe anonyme. Le code deviendrait
<?php
class Musket {
    private static $instanciated = false;
    private $var;
    
    public function __construct()
    {
        if (self::$instanciated === true) {
            throw new RuntimeException('Could not instanciate class');
        }
        self::$instanciated = true;
    }

    // Interdiction du clonage
    private function __clone()
    {
    }
    
    public function get(): int {
        return $this->var;
    }
    
    public function set(int $value) {
        $this->var = $value;
    }
}

class Factory {
    private $instance = null;
    
    public function getInstance()
    {
        if ($this->instance === null) {
            $this->instance = new Musket();
        }
        return $this->instance;
    }
}

$factory = new Factory();
$o = $factory->getInstance();
$o->set(5);

$factory2 = new Factory();
// Génère une exception "Could not instanciate class"
$o2 = $factory2->getInstance();


Dans le cas présent, on n'a pas un singleton, mais une classe mousquet (Musket Class). On pourrait d'ailleurs appliquer ce même principe sur la fabrique elle-même.

Un inconvénient de la classe mousquet, c'est la testabilité, car une fois qu'on l'a instanciée, on ne peut plus le faire, et donc tester plusieurs cas. Ce problème existe d'ailleurs avec le singleton, et il n'est pas rare d'y ajouter une méthode statique "resetInstance()". Dans le cas de la classe mousquet, on peut également ajouter une méthode du genre "rearm()" pour permettre de créer une nouvelle instance.

Maintenant, on peut se poser la question : Quel est l'intérêt de ça ? Le singleton est très souvent utilisé dans un souci de performance, et pour sa simplicité. De plus, on peut avoir une bonne hygiène de développement et toujours concevoir un code qui ne sera pas un plat de nouilles.

Autant, je suis d'accord sur la performance et la simplicité, autant je ne crois pas une seule seconde à son utilisation rigoureuse. Si vous êtes seul sur votre projet, que vous n'avez aucune date de livraison à respecter, oui, peut-être que cela serait possible. Dans tous les autres cas, mon expérience m'a prouvé à de très nombreuses reprises que ça ne tient pas bien longtemps : Deadline is a bitch !

Lire aussi cette excellent article de mon ami Hugo : Votre code est STUPID ? Rendez le SOLID !

F. Bouchery

A voir également :

Ce document intitulé «  Le problème du singleton  » issu de CommentCaMarche (www.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.