Les Design Patterns Singleton, Multiton et Alternatives en PHP

Récemment, j’ai exposé l’un des Modèles de Conception les plus répandu, le décrié Singleton.

Décrié car comme souligné dans mon article précédent, le Singleton souffre de sa simplicité de compréhension et de mise en oeuvre qui incite à l’utiliser aveuglément..

Le billet propose de faire un point sur ce modèle de conception et ses alternatives, avec comme d’habitude des exemples appliqués en PHP.

Pour rappel, un Singleton assure l’unicité de l’instanciation d’une classe en remplaçant son constructeur (privé) par une méthode statique qui retourne toujours la même instance.

En quoi est-ce un mal ?

1) Le Singleton a tendance à lier fortement les objets qui l’utilisent.

Du fait de son accessibilité universelle, le Singleton rend le développeur plus feignant qu’il ne l’est en réalité et l’incite à ne pas se donner la peine d’accepter les objets « Singleton » en classiques paramètres (principe d’injection de dépendance).

Ainsi, on trouvera régulièrement, dans tous les clients, des lignes du type

//quelque part
Log::getInstance ()->report ("j'aime les instances uniques");

au lieu du « plus souple »

   public function setLogObject (ILog $logObject){
      $this->_logObject = $logObject;
   }
   public function quiFaitDesChoses (){
      //... fait des choses puis le dit
      $this->_logObject->report ("Je préfère injecter mes dépendances");
   }

Dans le premier cas, on oblige notre classe cliente à connaître et à se limiter à notre Singleton alors que le second lui permet de s’en libérer.

2) Les Singleton compliquent les tests unitaires
De part leur caractère unique, les Singleton compliquent souvent les tests unitaires car ils rendent l’objet « statefull », alors que les tests unitaires doivent pouvoir s’exécuter les uns à la suite des autres, dans n’importe quel ordre…. et leur résultat ne doit pas être conditionné par un contexte particulier.

3) Un objet, une responsabilité
Ce n’est pas le rôle du Singleton de se préoccuper de son mode de création, l’objet ne devrait avoir à se préoccuper QUE de ce qu’il sait faire (pour éviter les multiples responsabilités menant aux super-classes, plus simplement nommées classes à tout faire)

4) C’est présumer par avance que notre objet ne peut avoir qu’une et une seule instance.

« Dans un ordinateur, il n’y a qu’un seul écran ! »

Ce qui était vrai il y a dix ans ne l’est plus nécessairement…. et s’il est envisageable aujourd’hui d’avoir un ordinateur avec plusieurs écrans, on peut supposer qu’il en sera de même avec notre objet.

5) En PHP, il n’existe pas de « vrais Singletons »
En effet, en PHP (outre manipulations particulières), le Singleton est limité à la portée de la page exécutée, à la portée du processus apache qui génère une réponse pour un utilisateur.

C’est à dire que si deux pages s’exécutent au même moment et utilisent notre Singleton, il existe bel et bien deux instances de cet objet.

Les fausses critiques

1) Le singleton est une variable globale déguisée
Même si en vulgarisant on pourrait arriver à cette conclusion, les Singletons n’incarnent heureusement pas l’ensemble des problèmes associés à l’usage des variables globales.

  • Ils n’occupent pas un nom de variable dans l’espace de nom
  • Contrairement à une variable globale, ils ne peuvent être écrasés par erreur
  • Ils sont forcément initialisés lorsque l’on fait appel à eux (alors que l’on ne peut pas vraiment savoir, dans notre code, si la variable globale est vraiment ce qu’elle devrait être)
  • Ils ne sont pas nécessairement instanciés si personne ne fait appel à eux (alors que l’on ne peut présumer par avance de l’utilisation ou non de notre variable globale, nous obligeant à l’instancier en début de programme).

2) C’est présumer par avance que notre objet ne peut avoir qu’une et une seule instance.
Oui, je me répète, j’ai déjà mis cette remarque dans les critiques du Singleton. Pour autant, n’oublions pas un principe de base : un programme est une modélisation, une abstraction simplifiée d’un problème pour en permettre la résolution automatique.

Inutile dans un programme de gestion de places de stationnement de modéliser les voitures dans leur ensemble et d’en connaître les moindres détails de motorisation, la marque de pneu et la composition de la gomme utilisée dans la roue de secours…. qu’il en existe une ou deux.

L’analyste va délibérément choisir de limiter sa conception, la difficulté étant de choisir avec sagesse la frontière entre souplesse et complexité.

Nous avions parlé Multiton

Un Multiton n’adresse qu’une seule des critiques présentée ci dessus, à savoir celle qui limite le nombre d’instance à une seule. Le Multiton permet bêtement de nommer ses instances « uniques ».

Un petit code vaut mieux qu’un long discours

class MultitonLog {
   /**
    * Les instances de notre objet
    */
   private static $_instances = array ();

   /**
    * Le classique constructeur privé comme le singleton
    */
   private function __construct (){}
   /**
    * Le clonage interdit
    */
   private function __clone (){}
 
   /**
    * La méthode de création
    * @param string $pInstanceName le nom de l'instance attendue
    */
   public static function getInstance ($pInstanceName){
      if (! array_key_exists ($pInstanceName, self::$_instances)){
          self::$_instances[$pInstanceName] = new MultitonLog ();
      }
      return self::$_instances[$pInstanceName];
   }    
}

L’utilisation du Multiton est alors quasi identique à celle d’un Singleton, à savoir :

MultitonLog::getInstance ('default')->action ();
MultitonLog::getInstance ('autreInstance')->action ();

Et mes alternatives ?

Même si l’utilisation des Singletons peut être tout à fait justifiée et être un vrai choix de conception, j’avoue que j’aime assez l’alternative des Fabriques qui s’occuperont elles mêmes de l’unicité des instances (ou de l’injection des dépendances).

Fabrique prenant en charge l’unicité des instances

class FabriqueDeSingleton {
   private static $_instanceDeSingleton = false;
   public static function getSingleton (){
      if (self::$_instanceDeSingleton === false){
         self::$_instanceDeSingleton = new Singleton ();
      }
      return self::$_instanceDeSingleton;
   }
}

Cette fabrique a plusieurs mérite :
1) Elle libère le Singleton de la responsabilité de création
2) Elle permet de passer d’un mode d’instanciation unique à un mode d’instanciation multiple simplement.

Elle n’assure par contre pas que les clients se limiterons à la fabrique pour obtenir une instance de l’objet, et n’assurera donc pas l’unicité de l’instance dans tout le programme.

Fabrique prenant en charge l’injection de dépendances

Pour aller plus loin et limiter la dépendance des objets clients, on peut également ajouter une fabrique pour l’injection de dépendances

class FabriquePourClient {
   public static function getClient (){
      $instance = new FabriquePourClient ();
      $instance->setLog (FabriqueDeSingleton::getSingleton ());
      return $instance;
   }
}

Cette fabrique à maintenant le mérite de supprimer les dépendances du code client à une quelconque Fabrique ou Singleton.

Alors ? que faire ?

Il n’existe comme d’habitude aucune réponse universelle, la meilleure solution est celle qui vous conviendra à vous et votre équipe, qui vous permettra d’atteindre les objectifs fixés en empruntant le chemin le plus direct.

  1. if (! array_key_exists ($pInstanceName, self::$_instances, true)){

    dans le multiton

    c’est quoi ce ,true ?

    • au temps pour moi, je ne sais pas pourquoi, j’ai voulu mettre une option [bool $strict] comme pour in_array et array_search….. j’ai corrigé, merci !

  2. Comme je l’explique ici, la plupart du temps, le singleton sert juste à rendre unique l’accès à une ressource.

    En générale, et à condition de gérer correctement la création de ces objets, un attribut de classe suffit.

    Surtout qu’en PHP, contrairement à Java ou à d’autre langage à objet, il y a une grosse spécificité : les objets ont un temps de vie ridiculement court !

    >1) Le singleton est une variable globale déguisée

    Effectivement le singleton à un pouvoir de nuisance heureusement moindre qu’une variable globale, il en a néanmoins une très forte odeur.

    • Surtout qu’en PHP, contrairement à Java ou à d’autre langage à objet, il y a une grosse spécificité : les objets ont un temps de vie ridiculement court !

      Très bonne remarque qui diminue en effet l’intérêt « performances / mémoire » que l’on pourrait attribuer à ces pratiques.

  3. Sujet trollesque s’il en est : « le singleton c’est un peu le mal ».
    Je ne vais pas m’attaquer au sujet, l’auteur a le droit de penser ça et chaque point est acceptable mais comme tout bon sujet trollesque le contraire est aussi vrai.
    2-3 exemples :
    – « Très bonne remarque qui diminue en effet l’intérêt « performances / mémoire » que l’on pourrait attribuer à ces pratiques. » trouvé dans un commentaire précédent : par expérience, l’utilisation d’un singleton dans une application PHP fortement chargée peut avoir un énorme impact sur les performances. Donner la liberté aux développeurs d’instancier de multiples fois la classe de log est une hérésie. Ou comment exploser les besoins en mémoire d’une application…
    – « j’avoue que j’aime assez l’alternative des Fabriques qui s’occuperont elles mêmes de l’unicité des instances » : très bonne idée que j’utiliserai surement à l’avenir. Mais lorsque l’on développe un framework et que l’on veut poser des barrières aux développeurs pour qu’il ne s’égare pas du droit chemin (un peu biblique tout çà), le singleton reste une valeur sûre.
    – « Les Singleton compliquent les tests unitaires » : pas vrai. J’ai écrit les tests unitaires d’une classe de vérification de paramètres la semaine dernière sans aucun soucis alors que c’est un singleton. La vrai plaie des tests unitaires (avec PHPUnit tout du moins) ce sont les constantes. Elles obligent pour rétablir un état vierge à créer un fichier par test. Mais c’est plus un problème de PHPUnit en fait. Au final, il vaut mieux se prendre la tête à écrire les tests pour avoir une classe performante et qui ne s’assimile pas à un pistolet sans cran de sureté à tous ces développeurs mal intentionnés 😉

    • Sujet trollesque s’il en est : « le singleton c’est un peu le mal ».

      Attention aux simplifications : mon sujet n’est absolument pas de dire que le Singleton est « un peu le mal », mon sujet est une réflexion globale sur l’utilisation des Singletons, mon sujet est de poser quelques questions pour alimenter une réflexion.

      Maintenant que ce point est éclairci, passons aux remarques 🙂

      l’utilisation d’un singleton dans une application PHP fortement chargée peut avoir un énorme impact sur les performances

      Tout à fait, et d’autant plus si l’objet transformé en Singleton dispose d’un mécanisme de construction coûteux. Toutefois, l’intérêt « diminué » (et non annihilé) réside dans le fait que plusieurs processus concurrents instancieront plusieurs singletons (donc une optimisation moindre que ce que cela pourrait être).

      De plus, le coût du Singleton n’est pas uniquement incarné dans sa construction mais aussi dans ses traitements => traitements qu’il faudra re-préparer d’une requête à l’autre (ce qui ne serait pas le cas avec un langage comme java ou le Singleton aurait une existence applicative globale).

      Donner la liberté aux développeurs d’instancier de multiples fois la classe de log est une hérésie.

      Non, je ne suis pas d’accord.

      En fonction de l’implémentation de ma classe de log, il est tout a fait envisageable d’en avoir plusieurs (tout est toujours une question d’implémentation et de choix de conception).

      Dans mes applications, je distingue souvent plusieurs types de logs (des logs applicatifs destinés aux utilisateurs, des logs techniques sur la base de données à à l’attention des DBA en cas de problème, des logs d’erreurs envoyés par mail aux développeurs des modules concernés, ….)

      Pour ma part, j’ai donc plusieurs objets de logs instanciés pour une même requête (pour la petite histoire, c’est une fabrique qui gère une instance unique des stratégies de log par type d’information).

      Mais lorsque l’on développe un framework et que l’on veut poser des barrières aux développeurs…

      J’ai pour ma part passé pas mal de temps à développer dans le milieu des frameworks. Encore une fois, les Singletons sont des choix de conception, ils peuvent être justifiés sans problème, je ne le remet pas en question. L’alternative des fabriques n’est pas toujours pertinent ou suffisant.

      Pour ma part, j’utilise encore des Singletons pour le contrôleur frontal, l’objet de de configuration générale, la requête, la session, les variables d’environnement, je permet même certaines situations hybrides ou les objets mettent à disposition une instance unique sans pour autant restreindre la double instanciation… (mais il arrive dans certains contextes que je regrette ces choix…. même pour la requête d’interrogation qui semble pourtant unique au prime abord)

      Il n’est souvent pas _nécessaire_ de poser des barrières via l’utilisation d’un singleton. On peut très bien supposer que ton Framework utilise les instances configurées via un registre, et qu’il se moque simplement des autres… ce qui pose à ton développeur la barrière de l’utilisation du bon composant s’il souhaite que cela fonctionne, tout en lui permettant de réutiliser pour d’autres objectifs ton objet.

      « Les Singleton compliquent les tests unitaires » : pas vrai

      Et pourtant si (mais pas tout le temps).

      Déjà, ils compliquent les tests unitaires « surtout » des codes clients (ceux qui utilisent le Singleton), pas forcément du Singleton lui même (le fameux caractère « statefull »). Si nous avions des codes clients qui fonctionnent par injection de dépendance, il serait très facile d’injecter un MockObject pour voir si notre « AuraitPuEtreUnSingleton » était appelé. Avec l’utilisation d’un Singleton, c’est difficilement le cas (sauf bien sûr si le Singleton est injecté à notre client à sa fabrication.)

      De plus, si la création de notre Singleton dépend d’un contexte, notre test unitaire sera incapable de tester la création avec un contexte correct suivi de la création avec un contexte incorrect => une fois que le contexte correct aura été testé, même en cas de mauvais contexte, la première instance sera retournée => les tests dépendent de l’ordre dans lequel ils ont étés exécutés (chose que l’on contournera avec des tests unitaires exécutés dans des environnements séparés…. ce qui les compliquent) (même si dans ce cas on peut comme tu le fais reprocher à PHPUnit de ne pas gérer ses tests dans des environnements distincts)

  4. @simon :
    > Donner la liberté aux développeurs d’instancier de multiples fois la classe de log est une hérésie

    Si un développeur fait cela, ne veut-il pas mieux lui expliquer pourquoi ca ne va pas ?

  5. Bonjour,
    Content de voir des réponses aussi détaillées et content de ce débat.

    Je ne vais pas pousser plus loin sur la plupart des points, les 2 points de vue sont exprimés. Si je revenais à la charge, on sera là dans le troll 😉

    Juste quelques petites clarifications :

    – Lorsque je parlais de performances sur un site fortement chargé, l’emprunte CPU est importante mais généralement la première limite atteinte concerne la mémoire. Réduire l’emprunte CPU est relativement simple avec des outils de cache tels qu’APC. Réduire l’emprunte mémoire requiert généralement une attention toute particulière dans le code. L’utilisation du singleton est alors une aide précieuse.
    Enfin, j’aimerais faire remarquer que PHP est aussi utilisé autrement que par un serveur HTTP. Dans ces cas, la réduction d’impact dont vous parlez n’existe pas.

    – Pourquoi pas plusieurs classes de log effectivement pour plusieurs utilisations mais une classe uniquement peut rendre tous les cas d’utilisation dont tu parles en étant en plus un singleton.

    -Enfin, pour répondre à Eric, oui éduquer les développeurs est une priorité mais prenons le cas d’un framework publique tels que ZF ou Cake. On dirait tout simplement que le framework a « des perfs pourries » avant de juger la façon dont il est utilisé.

    Ah une dernière chose, gerald, effectivement tu as réussi à lancer la réflexion sur le sujet 😉 Mais je n’abandonnerai pas l’utilisation des singletons tout de suite, mais je serai averti du coup.

    • Content de voir des réponses aussi détaillées et content de ce débat.

      De même

      Si je revenais à la charge, on sera là dans le troll

      La frontière est souvent difficile à maîtriser mais je pense que nous sommes encore du bon coté de la barrière 🙂

      Réduire l’emprunte CPU est relativement simple avec des outils de cache tels qu’APC.

      Je sais que ce n’est pas ce que tu as voulu dire, mais APC ne limite pas la charge CPU du script, il ne limite qu’une seule chose (en standard) : le temps de « compilation » des scripts (ce qui a pour effet de limiter la charge du serveur).

      Ainsi, plus il y a de classes / fichiers à charger, plus le gain sera notable. Le déroulement du script en lui même n’est pas impacté par APC. Toutefois, avec l’utilisation de frameworks complexes et / ou l’utilisation de nombreuses librairies, le gain en standard est effectivement euphorisant.

      Réduire l’emprunte mémoire requiert généralement une attention toute particulière dans le code. L’utilisation du singleton est alors une aide précieuse.

      Le Singleton est un « moyen » de réduire l’emprunte mémoire car il va distribuer une seule instance de lui même.
      C’est un moyen simple car il suffit de faire MonObjet::instance() plutôt que new MonObjet () avec toutefois les inconvénients cités plus haut.

      Pour limiter cette instanciation multiple, il existe les alternatives listées qui arriveront au même résultat (Fabrique / Injection de dépendances).

      Je note d’ailleurs dans ma todo list un billet sur l’optimisation en général en PHP…

      Enfin, j’aimerais faire remarquer que PHP est aussi utilisé autrement que par un serveur HTTP. Dans ces cas, la réduction d’impact dont vous parlez n’existe pas.

      phpcli ? => même combat
      phpgtk => je n’ai jamais utilisé outre un hello the world, mais convenons que c’est une utilisation très confidentielle.
      autre ? a ma connaissance, a part des projets expérimentaux de compilation des scripts (outre celui de facebook que je n’ai jamais essayé…. et ou je ne sais si il existe une portée applicatif), on ne peut pas dire que PHP dispose d’espaces applicatifs quels que soient ses usages.

      Pourquoi pas plusieurs classes de log effectivement pour plusieurs utilisations mais une classe uniquement peut rendre tous les cas d’utilisation dont tu parles en étant en plus un singleton.

      Il est compliqué de rentrer dans les détails, mais oui, tout est faisable en tout, tout est choix de conception.

      Pour ma part, je préfère « une classe = une responsabilité », je ne veut pas que mon log sache à la fois envoyer des infos par mail, dans la base de données, formater le tout en XML, le mettre en session pour un affichage dans une popup, fasse un affichage avec des informations de backtrace, …. je préfère une stratégie pour chaque (ce qui pour le coup limite l’emprunte du script généré au seul code nécessaire à un instant T).

      De plus, chose que je n’ai pas précisé, chaque comportement est réalisé sous la forme de plugin que chaque responsable de site à loisir d’installer ou non pour suivre l’activité de ses modules, et chaque responsable de site à loisir d’ajouter des comportements, et de les partager => une classe unique aurait provoqué des conflits ou aurait fini avec 200 options de configurations et des if dans tous les sens.

      Ah une dernière chose, gerald, effectivement tu as réussi à lancer la réflexion sur le sujet

      Merci, j’en suis heureux.

      Mais je n’abandonnerai pas l’utilisation des singletons tout de suite

      Moi non plus ! Tu verras d’ailleurs prochainement, dans un projet que je publierais dans les prochains jours, que la première fonctionnalité est réalisée sous la forme …… d’un Singleton 🙂

  6. C’est de l’ordre du détail, et peut être/surement plus une question d’habitude, mais dans ce cas je ne vois pas l’intérêt d’utiliser array_key_exists(), si l’instance est null c’est qu’il y a un problème. Pourquoi ne pas simplement faire :


    if(!isset(self::$_instances[$pInstanceName])) {

    C’est bien moins gourmand niveau perf… sinon merci pour ces articles fort intéressants…

    @ tchaOo°

    • Je ne sais plus, une bête erreur probablement. Peut être une habitude prise dans une ancienne version de PHP…

      En tous les cas le isset est en effet plus pertinent, même si le « bien moins gourmand » est très relatif et se mesure en millisecondes sur 100 000 appels 🙂

      • Oui et non les fonctions sur les tableaux sont assez gourmandes car php parcours ledit tableau…

        Pour array_key… c’est de l’ordre de 100ms sur 10.000 itérations ce qui n’est pas rien non plus et si je me souviens bien elle charge plus le cpu qu’avec isset (à revérifier sur les dernières version de php ça s’est pas mal amélioré).

        Après on est d’accord ce n’est pas cela qui plombera tout un code… 😉

        • Oui et non les fonctions sur les tableaux sont assez gourmandes car php parcours ledit tableau…
          Pour array_key…

          Dans le cas de array_key_exists il n’y a pas de parcours du tableau, c’est un accès direct.

          (cf sources)

          https://github.com/php/php-src/blob/94bea670de40c937d5926839d52dc262750921f4/ext/standard/array.c#L4997

          isset est plus a propros dans le cas présent car c’est vraiment ce que l’on cherche à tester, mais d’un point de vue perfs pures, même si on doit être a *2, je persiste à dire que l’impact est vraiment minime (sauf extreme micro optimisation).

          • Au temps pour moi pour le parcours de tableau je me suis mal exprimé je pensais notamment à in_array à ce moment là. Effectivement array_key.. ne parcours pas le tableau mais ce n’est pas non plus un appel direct et c’est là ou le bas blesse.

            Je m’explique, en fait vous avez pointé le problème sans le savoir :

            https://github.com/php/php-src/blob//ext/standard/array.c

            En tant que fonction, les fonctions sur les tableaux sont définie par l’extension standard directement compilée avec php. Comme toutes fonctions elles font donc appel à l’api zend pour travailler. Isset, comme empty/echo/etc… est un element du language il fait partie intégrante du moteur de php.

            Les résultats varient en fonction de la version, du compilateur et surtout du type de gestion de mémoire utilisé (ts/nts), le résultats peut passer de p * 2 ou 3 à p * 10 voir plus. Par exemple avec 10.000 itérations sur un tableau de 1000 entrées :


            > php : 5.5.24 (nts)
            > isset : 0.0011s
            > array_key.. : 0.0037s

            > php : 5.5.24 (ts)
            > isset : 0.0030s
            > array_key.. : 0.0470s

            > php : 5.6.7 (nts)
            > isset : 0.0020s
            > array_key.. : 0.0068s

            > php : 5.6.7 (ts)
            > isset : 0.0060s
            > array_key.. : 0.0550s

            Je m’étais rendu compte de cela en travaillant sur un script en cgi, une tache cron qui travaillait sur des grosses quantité de données.

            Encore une fois, ce n’est effectivement pas la fin du monde et comme vous l’avez dit le gain sera souvent peu/pas perceptible mais ce n’était pas le but de mon 1er commentaire c’était purement anecdotique. Si on optimisait un code juste en changeant une fonction de base cela se saurait, c’est juste un constat comme tant d’autres. 🙂

            Mais bon, là on s’éloigne du sujet d’origine bien plus intéressant que array_key_exists() 😀

            Bon week end…

          • Je m’explique, en fait vous avez pointé le problème sans le savoir :

            Je connais bien les différences ^^

            (je n’avais par contre jamais testé ces histoires de types de mémoire)

            Merci pour les benchs à jour, cela complète la discussion pour les lecteurs curieux !

  7. Pas de problème, avec plaisir… en fait je ne m’en était jamais rendu compte avant de bosser sur une usine à gaz en CGI… par contre je n’ai pas compris pourquoi la différence de temps d’éxécution peut être aussi grande même sur une mémoire partagée, mais c’est au delà de mes connaissances en C… 🙂

    Bref merci surtout à vous pour vos articles sur les DP… ils sont très bien rédigés… en tant qu’autodidacte, même si cela fait 15 ans que je code, la méthodologie me fait toujours défaut sur des projets complexe (refactoring mon amie :/), c’est donc un plaisir de lire des articles comme les votres à la fois accessible et précis (niveau ni débutant ni ingénieur).

    Cdlt…

  8. Pfuii il y a du level ^^

    Juste pour te dire un grand merci pour ton site !

Laisser un commentaire


NOTE - Vous pouvez utiliser les éléments et attributs HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>