Etes-vous atteint de Singletonite ? | 0 vote(s)
Par Emmanuel Deloget, jeudi 26 octobre 2006 à 13:33 :: Architecture, divers :: permalien #25
Tags: design pattern, singleton
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.

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.

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 14 juin 2007 à 16:14, par Christophe MOUSTIER
2. Le jeudi 14 juin 2007 à 19:45, par Emmanuel Deloget
3. Le samedi 23 juin 2007 à 13:25, par Christophe Moustier
4. Le jeudi 28 juin 2007 à 16:36, par Emmanuel Deloget
:: Fil rss des commentaires de ce billet ::
Ajouter un commentaire