26 oct. 2006

Etes-vous atteint de Singletonite ?

Ce billet est un peu spécial, car il s’agit de la traduction d’un triple billet écrit par Sean "Washu" Kent, l’un des modérateurs du site gamedev.net, pour son propre blog. Il m’a très sympathiquement permis de traduire ces billets – je l’espère pour votre plus grand plaisir. Je le remercie vivement.

A noter, avant de commencer, que ce billet est très emprunt d’un vocabulaire utilisé dans le petit monde des développeurs de jeux vidéo. Si vous avez des questions, n’hésitez pas à poster un commentaire. Les billets ont en outre été publiés à plusieurs jours d’intervalle, ne vous étonnez donc pas de retrouver des exemples similaires dans des billets pourtant concomitants. Gardez aussi à l'esprit que j'ai repris ses mots - et donc ses opinions. Je peux les partager, ou non.

Enfin, si vous souhaitez commenter ce billet, n'hésitez pas à écrire votre commentaire en Anglais (si vous souhaitez citer une portion du texte, vous pouvez la retrouver sur la version anglaise). De cette manière, l'auteur original pourra vous répondre si il le souhaite. C'est exceptionnel, mais l'auteur ne parlant pas français, cela peut aider. Bien évidemment, je ne vous oblige à rien - vous pouvez tout aussi bien formuler vos commentaires en français, je me chargerais alors de le faire passer à Washu afin qu'il réponde si il le souhaite.

Première partie

Pourquoi êtes-vous touchés par la Singletonite ?

Non, ce n’est pas un sujet récent, et en fait il tourne dans mon esprit depuis déjà un certain temps. Mais je pense que je dois probablement expliciter mes sentiments envers cette infection particulièrement virulente qui tend à se propager comme le feu sauvage parmi les moins expérimentés (peut-être toi).

Définition du Singleton

The patron de conception Singleton est défini comme étant un moyen de "s’assurer qu’une classe a une unique instance, et définir un point d’accès global à cette instance"[1]. Principalement, vous avez restreint la création de l’instance à un unique endroit dans le code. Dans la plupart des implémentations, cet endroit fournit aussi le point d’accès global au singleton.

La Grande Maladie : la Singletonite

Donc, qu’est-ce que la Singletonite ? Il s’agit d’une maladie découverte par Joshua Kerievsky. Il décrit sa trouvaille dans son livre Refactoring To Patterns[2]. Pour simplifier, c’est lorsqu’une personne devient accroc au patron de conception Singleton.

Malheureusement, cette maladie contamine facilement celui qui n’est pas préparé. Il faut parfois plusieurs mois, voire plusieurs années avant d’en guérir, et beaucoup de ceux qui ont été touchés le seront à vie. A cause de ce problème, il est urgent d’éviter les Singletons à tout prix - pensez à ses pauvres débutants infortunés qui seront ainsi sauvés d’un cruel destin.

Candidats pour des Singletons

Gestionnaire graphique
  • Pourquoi : j’ai besoin d’utiliser mon gestionnaire graphique partout dans mon code, et je n’ai besoin que d’un seul objet de ce type
  • Réponse : non, c’est faux. Vous n’avez besoin d’avoir accès au gestionnaire graphique que dans des portions très localisées du code qui sont situées un peu partout à cause d’une conception défectueuse et de votre échec à refactoriser. Si vous appliquez les méthodologies de conception appropriées, vous vous apercevrez qu’un Singleton n’est pas nécessaire. De plus, si vous n’avez besoin que d’un seul gestionnaire graphique, veillez à n’en créer qu’un.
Gestionnaire de son
  • Pourquoi : pour les mêmes raisons que le gestionnaire graphique, je l’utilise partout et je n’ai besoin que d’une seule instance.
  • Réponse : la même que ci-dessus, un refactoring approprié et des techniques de conceptions peuvent vous permettre de consolider le code (dupliqué) requis pour utiliser le gestionnaire de son en un petit nombre de classe, éliminant du même coup le besoin d’un Singleton.
Gestionnaire d’entrées[3]
  • Pourquoi : parce que je suis un peu diminué
  • Réponse : si vous utilisez un singleton pour gérer les entrées, vous avez un gros problème. Plus précisément, la somme de code requise pour accéder au gestionnaire d’entrée est de l’ordre de... une fonction. Peut être plus, cela dépends de votre conception. En tout cas, une classe au plus devrait accéder à ce gestionnaire – en fait, il ne devrait être par personne, excepté peut être une classe d’affection d’actions[4] qui à son tour n’a pas besoin d’être un singleton étant donné le peu de classes qui y accèdent.

La liste continue. Dans presque tous les cas, le raisonnement qui amène à l’utilisation d’un Singleton est soit: il serait “trop difficile” de passer un pointeur en paramètre un peu partout, ou il n’y a besoin que d’une seule instance de la classe. Aucun de ces raisonnements n’est une raison valide pour utiliser ce patron de conception : un refactoring approprié peut éliminer la majorité des cas de passage de paramètres qui sont nécessaires pour amener un objet là ou il doit être utilisé. De plus, cela permettra de centraliser le code commun et de supprimer une grosse partie du code dupliqué. A propos de l’excuse de l’instance unique : et alors ? Créez là, cette instance unique. Si vous devez laisser cette responsabilité à quelqu’un d’autre, écrivez la documentation nécessaire ou utilisez une factory – qui peut être un singleton (bien que ça ne soit pas nécessaire, le patron monostate pouvant faire l’affaire (notez que je n’ai pas dit que vous deviez utiliser soit l’un soit l’autre)).

Fruny aime les Singletons[5]. Ils sont ce qu’il préfère programmer dans son langage favori, Python. Il les utilise aussi beaucoup en C++ pour déterminer l’ordre de création de ses instances statiques. Il s’agit là d’un abus du patron de conception, et il N’EST PAS NECESSAIRE dans ce cas. Mais comment passer outre, direz-vous ? C’est simple : ne créez pas de variable globale qui dépendent d’une autre variable globale. Si vous faites cela, il est probable que vous ayez un sérieux problème dans votre conception, et vous devriez penser à refactoriser tout ça.


Seconde partie

Le but du patron de conception Singleton est de s’assurer qu’une classe a une unique instance, et offre un point d’accès global à celle-ci.

Il semblerait que beaucoup de personnes oublient que le patron de conception contient cette définition en entier. Beaucoup de "Singletons" ne sont des Singletons que pour utiliser une seule de ces deux propriétés. Reprenons les exemples énumérés plus haut, et continuons à partir de ça.

Gestionnaire d’entrée

Voilà peut-être le problème le plus important. L’unique raison pour laquelle on fait de lui un Singleton est qu’on souhaite garantir qu’une seule instance sera créée. Bien évidemment, en faisant cela, le programmeur a de manière bien pratique oublié la moitié de la définition. Le problème dans ce cas est un problème de visibilité. Dans son effort continu visant à empêcher les autres d’abuser leur code, il a introduit un artefact bien pire, car maintenant le gestionnaire est global.

Comment empêcher la création de deux instances d’une même classe ? C’est très simple : ne le faites pas. A la place, gérez les erreurs et envoyer l’exception appropriée. Puis, dans votre documentation indiquez clairement qu’une unique instance doit être créée.

Gestionnaire de son

Celui là est certes moins offensant que le gestionnaire d’entrée, mais il reste quand même sérieux. Ici, la raison pour laquelle on en fait un singleton est pour le rendre global. Encore une fois, notre programmeur à oublié la moitié de la définition.

Premièrement, on remarquera que ce gestionnaire n’a pas besoin d’un point accès global, le code qui l’utilise étant généralement assez contenu. En appliquant les techniques de refactoring, on peut déplacer ces portions de code dans un ensemble approprié de classes – la visibilité du gestionnaire de son est donc réduit d’une portée globale à une portée locale.

Une question se pose : pourquoi le programmeur l’a-t-il implémenté dans un premier temps en utilisant le patron Singleton, puisqu’il souhait une variable globale ? La réponse est simple : le programmeur voulait une variable globale, mais il se sentait coupable à l’idée d’utiliser une "vraie" variable globale. Il a donc poussé son raisonnement au bout, et il a encapsulé cette variable dans un objet – il a OOifié sa solution. Le problème est qu’en fait sa solution n’est pas orientée objet du tout – en fait, il s’agit effectivement d’une solution qui est tout sauf orientée objet.

Gestionnaire graphique

Celui-là est un singleton légitime : le programmeur souhaitait manipuler un objet via une unique instance possédant un point d’accès global. Toutefois, comme dans les deux exemples ci-dessus, il y a le problème de la visibilité : le gestionnaire graphique n’est utilisé que dans une faible portion du code, et bien que ce code puisse être disséminé un peu partout, il n’en reste pas moins une petite portion de code (si ce n’est pas le cas, vous n’êtes pas en train de programmer un jeu mais un moteur de rendu graphique).

Eviter les Singletons

Quel est donc le réel problème avec les Singletons ? Pour paraphraser Kent Beck :

Le problème réel avec les Singletons est qu’il vous donne une très bonne excuse pour ne pas avoir à penser à la visibilité réelle d’un objet.

Rappelez-vous, un Singleton est un objet global. Une fois qu’un objet a été transformé en Singleton, son état ne peut plus être ni garanti, ni aisément testé. Tout le monde peut (et va) changer cet état.

Alors, comment est-ce qu’on peut éviter ce patron de conception ? C’est simple : vous appliquez un refactoring[6]. Ce faisant, vous obtenez deux résultats : le premier est que vous simplifiez le code. Il devient ainsi plus facile à maintenir, étendre, lire et cela facilite la réduction des duplications. En second, votre code est plus orienté objet, et la visibilité des objets devient plus claire. Lorsque vous refactorisez votre code vous pouvez centraliser le code qui utilise un objet (qui est un Singleton) de manière à supprimer le besoin d’une visibilité globale. Il passé ainsi de l’état de variable globale à celui de variable locale ou variable membre. Lorsque vous ne pouvez pas centraliser le code, passez l’objet souhaité en paramètres.

Maintenant, je ne dis pas que vous ne devez jamais utiliser le patron de conception Singleton. Au lieu de ça, je dis que vous devez être très attentive aux implications entrainées par le fait de faire d’un objet un Singleton. Vous devez vérifier que l’objet a besoin d’être visible globalement ou si vous utilisez cette technique pour passer outre une mauvaise conception. Si vous êtes dans ce cas, refactorisez la conception pour l’améliorer et la rendre plus Claire. Pour utiliser les mots de Martin Fowler[7] :

Rappelez vous que toute variable globale est toujours coupable jusqu’à ce que vous ayez prouvé qu’elle est innocente.


Troisième partie[8]

Commentaire de stormrunner
Je réalise que c’est peut être faire prévue d’ignorance, mais j’ai quand même besoin de le demander : dans quel cas peut-on utiliser un Singleton ? Tu as – de manière brutale – détruit toutes les bases d’une utilisation du patron Singleton dans le cadre du développement de jeux. Le dernier bastion de défense, tel que je le vois, existe dans son application aisée pour l’écriture d’un gestionnaire de mémoire ou d’un système de messages. Mais même dans ces cas cela ressemble à de la fainéantise (c.f. Enginuity, signaux et slots)... Est-ce que le Singleton est applicable dans une quelconque tâche reliée à la programmation de jeux ?

Celà peut-il être ? Est-ce que mes yeux me jouent des tours ? Ou est-ce que quelqu’un a-t-il réellement compris ce que je souhaitais exprimer dans ces billets ? Je répondrai à ta question bientôt, mais d’abord…

Commentaire de _the_phantom_
@stormrunner
Le seul cas d’utilisation d’un Singleton auquel je peux penser est un système de journal, pour lequel on peut argumenter sur le fait que l’on a besoin d’une unique instance et d’une visibilité globale afin de pouvoir logger de partout.

J’attends maintenant que Washu démonte cet argument...

J’ai deux opinions sur cette idée. La première est : KEEL Phantom[9]. La seconde est plus intéressante.

On souhaite logger des événements, ces événements pouvant être des informations, des avertissements, des erreurs ou des flux de débogage. Comment faire ? La première solution, un choix évident, serait de créer un Singleton de logging avec une fonction qui prendrait un drapeau, quelque chose comme ça : Write(LL_DEBUG, StringBuilder(“2 + 2 = {0}”) % result);. Cela semble être une approche raisonnable, et peut être parfaitement acceptable pour un premier système de journal. Mais complexifions un peu les besoins. Premièrement, je voudrais pouvoir logger vers des flux multiples. Ensuite, je voudrais pouvoir indiquer le niveau de log pour chaque flux. Les niveaux sont classés dans une structure hiérarchique, comme cela :

  • Debugging
  • Informational
  • Warnings
  • Errors

Donc si vous sélectionnez le niveau Warnings, votre flux recevra les informations Warnings et Errors. Si vous sélectionnez Debugging, vous allez tout recevoir dans votre flux. Maintenant, la question : est-ce que cette nouvelle conception peut se satisfaire d’une solution à base de Singleton ? Dans ce cas, oui. Est-ce qu’une telle solution est nécessaire ? Bien évidemment que non, mais dans ce cas je considérerais quand même que l’utilisation du patron de conception Singleton est appropriée, pour la simple raison que le besoin d’accéder au système de journal depuis n’importe quel endroit du code existe, et qu’un refactoring n’est pas une option.

Diagramme de classe pour un système de journal

Une autre utilisation possible du Singleton pourrait être une factory concrète exportée d’une DLL via une interface (voir le diagramme ci-dessous). Dans ce cas les détails d’implémentation de la classe seront cachés du client. Vous aurez aussi la possibilité de contrôler le nombre d’instanciation de l’interface que la factory autorise. Dans ce cas, vous pouvez atteindre votre but (garantir une instance unique d’une classe – objet graphique, gestionnaire d’entrée), mais si vous souhaitez plusieurs instances vous n’êtes pas bloqués par les limitations du Singleton, il vous suffit juste d’utiliser l’interface de la factory pour obtenir un nouveau pointeur.

Diagramme de classe pour une factory


Notes

[1] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley Professional, 1995, ISBN 0-201-63361-2.

[2] Joshua Kerievsky, Refactoring to Patterns, Addison-Wesley Professional, 2005, ISBN 0-321-21335-1

[3] note: dans ce cas, Washu parle des entrées utilisateur (clavier et souris)

[4] dans le monde du jeu vidéo, il s’agit d’un système permettant de faire correspondre les entrées utilisateurs aux actions qui leur sont associées ; l’automate résultant est souvent très simple, d’où le commentaire de Washu.

[5] Fruny est un autre modérateur de gamedev.net ; cette remarque était peut être valable l’année dernière, lorsque ces billets ont été publiés, mais je n’ai aucune idée du sentiment actuel de Fruny envers les Singletons.

[6] Martin Fowler, Refactoring: Improving the Design of Existing Code, Addison-Wesley Professional, 2000, ISBN 0-201-48567-2

[7] Martin Fowler, Patterns of Enterprise Application Architecture, Addison-Wesley Professional, 2003, ISBN 0-321-12742-0

[8] cette partie cite des commentaires aux deux précédents billets de Washu. Je n’ai pas inclus ces commentaires en entier - pour les lire en intégralité, je vous suggère d’aller visiter le lien que je vous ai donné ci-dessus.

[9] _the_phantom_ est encore un autre modérateur du site gamedev.net ; entre modérateurs, il est courant de voire surgir des blagues et autres sarcasmes.

Commentaires

1. Le jeudi, juin 14 2007, 16:14 par Christophe MOUSTIER

Pourquoi un singleton doit-il être "global" ?
Concernant la double propriété énoncée en début de 2nde partie (unicité et globalité), cela veut dire que si un classe encapsulée propose un l'unicité et la "globalité" uniquement dans un module (namespace, classes héritées, ou autres), alors ce n'est pas un singleton ?

Il me semble que la technique d'un "GetInstance()" statique est l'essence même d'un singleton et du coup, je ne voie pas bien la "honte" qu'il y aurait à disposer d'un namespace encapsulant ce GetInstance (avec un mutex pour bien faire les choses)...

Pour moi, le Singleton est avant tout (comme son nom l'indique), une représentation d'un accès à une ressource unique. Les conditions d'accès sont à lier au principe d'encapsulation présenté dans un autre billet sur ce site.

2. Le jeudi, juin 14 2007, 19:45 par Emmanuel Deloget

Bonne question, je te remercie de l'avoir posé.

Un singleton est global - parce qu'un singleton qui ne serait utilisé que localement n'a guère de sens. Si il est utilisé localement, alors il vaut mieux (d'un point de vue design) en faire une variable locale (à la classe, au namespace, à la fonction, etc), ça me parait plus naturel. La raison invoquée : si une variable globale n'est utilisée que localement, à quoi bon utiliser une globale ? Bien évidemment, cela ne règle pas le problème de l'unicité, mais celui-ci est dans la plupart des cas un faux problème (mis à part l'accès aux ressources hardware d'une machine, je ne vois pas ce qui (toujours au niveau du design) justifierai qu'une classe soit unique; et même dans ce cas, on peut imaginer bon nombre de scénarios qui n'ont pas besoin de faire appel à la notion d'unicité de la ressource; exemple: les flux du C++ (cout, cerr, clog)).

Bien évidemment, il se pose aussi la question de savoir ce qu'est une variable globale - c'est à dire: à partir de quand peut-on la considérer comme globale ? Au niveau de l'application, une variable globale est accessible dans toutes les parties de l'application (c'est la variable du type CWinApp d'un programme MFC). Mais on travaille aussi à des niveaux de granularité inférieurs. Qu'en est-il du module (librairie statique ou dynamique): est-ce qu'une variable qui est utilisable partout dans un module est globale ? Pour ma part, je dirais que oui. Au niveau du namespace, la question ne se pose pas - car le C++ ne pose pas de restriction sur l'accès aux variables déclarées dans un namespace - on remonte donc au niveau du module.

Peut-on descendre encore plus bas ? Je dirais que non. Et je m'explique.

Bien qu'au niveau du management des ressources par le compilateur et par l'OS une variable globale et une variable statique sont similaires, elles sont - au niveau du design - complètement différentes. La variable globale a un impact global sur le design - tout le monde va l'utiliser. La variable locale n'a aucun impact sur le design - uniquement sur l'implémentation. Si une statique de classe (ou une variable statique dans une fonction) n'est pas accessible à l'extérieur de cette classe ou de cette fonction, elle fait partie des détails d'implémentation - il existe probablement une autre implémentation qui a les même propriétés et qui ne changerait pas l'interface actuelle. Si elle est accessible à tout le monde, alors il s'agit d'une variable globale.

Etrangement, pour accéder à une ressource unique, j'aurais tendance à préférer faire confiance à l'OS, et me passer de tout artifice de compilation. Cela me permet ainsi d'instancier plusieurs fois la classe gérant cette ressource, chaque fois avec des paramètres différents. Si je tente deux accès concurrent à la ressource, l'OS me dira probablement que je suis un peu niais (et, dans le fond, me connaissant comme je me connais, il n'aura pas tout à fait tort). Si je me limite à un singleton, je n'instancie qu'une seule fois cette ressource, et il me faut jouer sans cesse avec les paramètres pour utiliser le device dans différents modes. Au final, je ne suis pas sur que le code soit plus facile à lire.

A plus !

3. Le samedi, juin 23 2007, 13:25 par Christophe Moustier

ta vision de la globalité me gène car elle fait abstraction de la relativité de l'encapsulation : après tout, si une variable est globale dans une application, elle reste "privée" pour les autres processus ; et en poussant ton raisonnement jusqu'au bout, seuls (?) les objets distribués ne pourraient avoir l'AOC "Singleton" ?

il se peut que je n'ai pas saisi toutes les subtilités de ton argumentaire, mais je n'ai pas été convaincu monsieur manu. ;^)

4. Le jeudi, juin 28 2007, 16:36 par Emmanuel Deloget

Dans mon explication je n'ai placé une limite basse à la notion de globalité - on ne parle pas de globale à une classe (ou du moins, c'est un abus de langage car on devrait parler de variable membre ou de propriété). On peut imaginer une instance globale à un namespace - en C#, par en C++ (Enfin, si C# permet de définir des instances de variables en dehors d'une classe - mes connaissances du langage restent très parcellaires). On peut aussi imaginer une instance globale à un module ou à une application - et pourquoi pas à un système, voire un ensemble de système - et en poussant le raisonnement, à l'ensemble des systèmes.

Les instances distribuées peuvent aussi être des ressources uniques accessibles globalement - si j'utilise DCOM, je peux sans problème m'assurer de l'unicité d'une ressource, quelque soit le nombre de plateforme qui vont tenter d'y avoir accès. Pour autant, ces instances méritent-t-elles le nom de variable globale ? Je trouve que c'est pousser un point loin le vocabulaire du programmeur. De même, je n'appellerais pas une telle instance un singleton - même si la sémantique est proche, voire identique - tout comme je me refuse à dire qu'un site web est un singleton, même si il est unique sur la toile et accessible dans le monde entier.

Pour être honnête, il se trouve que j'ai du mal à expliquer ce qu'est une variable globale sans dire "ben, c'est une variable, elle est globale". Alors je vais me reposer sur les définitions de Wikipedia: en français et en anglais. Je juge la définition française trop restrictive (il va faloir que je crée un compte wikipedia un de ces jours), et je trouve la définition anglaise un peu trop permissive, bien que plus juste que la version française - la notion de portée est importante.

est-ce que je suis plus clair ? =)

Ajouter un commentaire

Les commentaires peuvent être formatés en utilisant une syntaxe wiki simplifiée.

Fil des commentaires de ce billet