18 août 2011

De la gestion des gestionnaires

On conçoit aisément qu'il est très difficile de bien concevoir une application ou une libraire : cela demande des connaissances pointues en design ainsi qu'une imagination débordante. Par contre, il est très facile de mal faire : il suffit de se laisser appeler par les sirènes des différents pièges qui, nonchalamment, s'installent sur notre route.

Ce billet traite de l'un de ces pièges : l'omniprésent gestionnaire, qu'on retrouve à peu prêt partout, et dont on sait peu de choses.

Informatio errarum est

"Une carte n'est pas le territoire" disait Alfred Korzybski[1]. Les mots, en tant que telle, ne sont que des abstractions : le mot "chaise" n'est pas la chaise, et il ne définit pas non plus l'objet lui même (chose impossible : seul l'objet est l'objet ; toute autre définition est incomplète). Le mot "chaise" a une réalité différente de l'objet en lui-même : il permet à son utilisateur une généralisation qui désigne une chaise en particulier sans que celle-ci ait besoin d'être décrite complètement.

Le problème se pose de manière évidente lorsque nous utilisons des abstractions (le mot) en lieu et place de la chose réelle (la chaise). Il est plus ardu d'en voir les implications lorsque le mot désigne une abstraction de plus haut niveau (par exemple, et complètement au hasard, le mot gestionnaire face au concept gestionnaire).

Que décrit-on lorsque on utilise des mots qui parle de concepts abstraits ? Quelle généralisation fait-on ? La sémantique générale professée par Korzybski nous apprends que ces généralisations sont bâties en relation avec d'autres généralisation : une définition dans un dictionnaire peut être vu comme une super-carte qui lie entre elles d'autres cartes. A ce titre, l'étude de la définition[2] du mot "gestionnaire" est intéressante :

gestionnaire (adjectif): qui a trait à la gestion
gestionnaire (nom): personne chargée de la gestion
gestion (nom féminin): action d'administrer, de gérer. Synonyme: administration
administration (nom féminin) : action de gérer des biens ou affaires privées ou publiques.
administrer (verbe transitif): gérer des affaires publiques ou privées.
gérer (verbe transitif): administrer ses affaires ou les affaires d'autrui.

J'ai laissé de coté les définitions plus précises ou les synonymes qui s'écartent de notre but (par exemple, intendance est noté comme un synonyme de administration, et concerne l'administration des biens et finances de l'armée ou d'une collectivité ; on s'éloigne de notre sujet).

Ce qui est notable, c'est que la définition devient cyclique. C'est l'un des points remarqué par Korzybsky: la circularité est l'une des caractéristique générique du savoir humain.

L'un des problèmes soulevés par cette constante est que certains mots doivent avoir une définition implicite connue de tous afin de pouvoir être définis. Dans notre exemple, il devient impossible de définir le mot "gestionnaire" sans admettre un sens premier qui n'est pas précisé.

Je n'ai pas choisi ce dictionnaire au hasard : outre le fait d'être en ligne, il permet d'établir rapidement un cycle dans la définition du mot ; il est évident que plus on agrandit le cercle, plus on a une idée précise du sens du mot défini. Car - autre constante de notre mode de raisonnement - nous avons une forte propension a créer nous même des liens qui ne sont ni nécessairement apparents, ni même logiques. Bien souvent, nous travaillons par inférence : si je lie le mot gestionnaire à ses synonymes, je me fait une idée de la signification de ce mot, sans pour autant l'avoir défini.

Oui, et alors

On en revient au sujet premier de notre article : qu'est-ce qu'un gestionnaire ? Dans de domaine de l'informatique, personne n'est plus capable de donner une définition précise de ce mot que ne l'est un dictionnaire d'en donner une définition non circulaire. L'abstraction sous-jacente d'un "gestionnaire de X" est impossible a assimiler correctement car elle n'a pas de sens propre. On se heurte ici aux limites de notre propre langage.

Si on est pas capable de lui donner une définition correcte, on peut quand même toujours la prendre pour ce qu'elle est intrinsèquement : une super-carte liant plusieurs cartes entres elles - ou, pour parler de manière moins symbolique, si on ne peut pas définir un gestionnaire, on peut tout de même tenter d'en donner une description en s'appuyant sur les éléments qui le compose.

Prenons un exemple récurrent - si récurrent qu'on croirait presque y déceler l'existence d'un design pattern, alors qu'il faut en fait y voir un possible resign pattern : le concepteur décrit une classe resource_manager qui devra, à terme, implémenter les fonctionnalités suivantes :

  • à la demande de l'utilisateur, une instance de resource_manager va charger une ressource au format souhaité à partir d'un système d'entrée sortie (disque, mémoire).
  • une fois la ressource chargée, une demande subséquente de la même ressource réutilise les données déjà chargée, afin d'optimiser les temps de traitement.
  • lorsqu'une limite de consommation mémoire a été atteinte, les ressources qui n'ont pas été demandée récemment seront déchargées pour permettre à de nouvelles ressources d'être chargées.

Ceux qui programment (pour le plaisir ou de manière professionnelle) des jeux vidéo reconnaîtrons cette structure, que l'on retrouve sous plusieurs formes : gestionnaire de textures, gestionnaires de fichiers son, etc.

Bien que ça ne soit pas forcément visible au premier coup d'oeil tant ces fonctionnalités sont entremêlées, une implémentation de la classe resource_manager qui utiliserais ce cahier des charge risque de poser de nombreux problèmes . En premier lieu, le nombre de responsabilités de cette classe est trop élevé : j'en compte au moins 2 dans cette courte description.

  • un système de génération d'instances d'objet (autrement dit, une factory).
  • un système de stockage des objets déjà créé (autrement dit, une collection ou un cache).

On peut aussi y rajouter, de manière moins évidente, un système de contrôle de la durée de vie des objets - un tel système peut toutefois être vu de manière tout a fait correcte comme étant un des paramètre fonctionnel du cache.

Bien souvent, le gestionnaire typique intègre en plus bien d'autres fonctionnalités. Parmi les exemples que j'ai déjà rencontrés :

  • un gestionnaire de son qui, en plus de contenir une factory et un cache, permet aussi d'accéder au périphérique dédié afin de faire un rendu sonore selon des paramètres précis.
  • un gestionnaire de texture (encore une fois factory + cache) qui permet en outre de créer des textures procédurales ex-nihilo.

Toutes ces fonctionnalités supplémentaires imposent une complexité croissante du code, des dépendances accrues qui empêchent une réutilisation des différentes fonctions, et provoquent à terme une fragilisation du code - par exemple celui-ci peut se retrouver à devoir gérer des cas particuliers de plus en plus nombreux.

Des problèmes en pagaille

Vous l'avez certainement compris, les gestionnaires ne sont pas une panacée - malgré leur omniprésence, ils sont généralement le symptôme apparent de problèmes de conception logicielle plus profonds. On peut en sortir la règle suivante :

L'existence d'au moins une classe "gestionnaire" dans une architecture orientée objet démontre l'existence de problèmes dans cette architecture

Une fois que j'ai énoncé ça, je n'ai encore rien dit. Quels problèmes ? Comment vont-ils se manifester ?

De manière quasi-systématique, un gestionnaire va à l'encontre du Principe de Responsabilité Unique - je n'ai pas besoin de revenir sur les inconvénient d'une telle transgression, ils sont déjà abordés dans des billets plus anciens.

Dans une moindre mesure, il n'est pas rare de s'apercevoir que le Principe Ouvert/Fermé est lui aussi violé. Généralement, le code du gestionnaire est monolithique alors même qu'il est composé d'une multitude de fonctionnalités différentes. Étendre le gestionnaire (par exemple pour ajouter un nouveau format de fichier) devient rapidement une gageure - et ce malgré le fait que de telles extensions peuvent arriver très tard dans le cycle de développement (j'ai déjà vu le cas ou il est devenu nécessaire de supporter un nouveau type de fichiers de modèles 3D spécifiquement optimisé pour une plateforme donnée et développé sur le tard lorsqu'il est fut évident que l'impact de la lecture des fichiers de modèle sur le temps d'exécution était trop important).

De plus, les gestionnaires sont des entités très concrètes, mais le fait de les sur-gonfler de fonctionnalités en fait rapidement des objets qui sont utilisés un peu partout dans le code : ce sont des objets stables (la définition de stable, concret et les implication derrière ces définitions sont indiquées dans cet article).

A ces problèmes, il faut ajouter qu'une fois le gestionnaire suffisamment gros, il devient plus ou moins évident qu'une seule instance sera nécessaire dans l'application, et qu'on doit pouvoir accéder à cette instance de n'importe où dans le code. Nombreux sont les cas où les gestionnaires sont accessibles via des singletons, ce qui revient rapidement à multiplier les problèmes de maintenance : par exemple, dans un environnement multithread, il faut maintenant correctement protéger l'accès au gestionnaire, ce qui introduit généralement un système de synchronisation - qui lui même peut poser problème notamment au niveau temporel : le lien existant entre tous les composants du gestionnaire peut induire des problèmes de synchronisation grave (jusqu'au deadlock) ou des ralentissements imprévisibles.

Reste une question à laquelle il nous faut répondre : peut-on résoudre ces problèmes en éliminant la (ou les) classe(s) "gestionnaire" ?

Disparaît, démon !

Sans gestionnaire, je réduis de manière importante les dépendances entre mes classes utilitaires - ma classe de cache n'a pas besoin d'être liée à ma classe factory ; les deux peuvent être utilisées séparément, et deviennent des candidats raisonnablement bon pour être généralisées (ce qui va diminuer l'incidence de changements si les classes sont stables) - ce qui donne une ouverture supplémentaire au code, permettant de suivre le Principe Ouvert/Fermé. De plus, les interfaces qu'elles offrent sont nécessairement plus simple, et il devient aisé de respecter le Principe de Responsabilité Unique.

Le fait d'avoir des objets plus réduits en termes de fonctionnalité permet aussi de mieux contrôler leur réutilisation. La localité des utilisations est accrue, ce qui supprime la nécessité d'un accès global. De plus, leur unicité peut être remise en cause - pourquoi ne pas avoir deux caches de données, deux factory (pour des usages différents), etc. ? Il n'y a alors plus besoin de singleton.

Bref. Sans une analyse très pointue, on démontre assez aisément que ne pas utiliser de classes gestionnaire est systématiquement plus avantageux que le contraire.

Conclusion

Certains pourront se demander comment sont liés les deux parties qui composent cet article. En fait, et bien que ça ne soit pas vraiment évident, ce lien existe et il est très simple : dans une architecture orientée objet, un gestionnaire pose des problème à cause même de sa non-définition. Sans définition précise, on est limité dans notre compréhension par l'interprétation (obtenue par inférence) qu'on fait de ce qu'est un gestionnaire - hors, un gestionnaire, c'est un peut tout et n'importe quoi. Du coup, les classes qui implémentent des gestionnaires font un peu tout et n'importe quoi, sans grande logique, ce qui finit par contaminer des grosses parties de l'architecture mise en place (il suffit de regarder l'architecture générale de Ogre3D pour se rendre compte de ce phénomène : singletons, gestionnaires et god classes sont un lot commun ; je ne dis pas ça pour le plaisir de dire du mal ou par manque de respect - c'est juste une constatation). La sémantique nous donne quelques clefs importantes pour comprendre le sens des mots et l'implication qu'a ce sens sur l'utilisation des mots en question. Dans certains cas - comme celui qui nous intéresse - elle nous permet de mettre en avant le fait que le sens du mot en question est avant tout intuitif, et trop peu certain pour être véritablement utile dans un programme (qui est, je le rappelle, une construction mathématique).

Pour ma part, cette raison est à elle seule suffisante pour ne pas introduire de classes de type gestionnaire dans mon code.

Commentaires

1. Le jeudi, août 18 2011, 18:56 par 3DArchi

Salut,

Gestionnaire (ou son cousin d'outre-manche manager) sont des mots-valises : le problème n'est pas lié à la circularité de leur(s) définition(s) ni même à leur abstraction (un gestionnaire, ça peut être le type dans ta banque qui s'occupe de ton compte, pas plus abstrait qu'un garagiste ou un ingénieur que tu sais bien mieux identifier). C'est plutôt qu'ils sont vidés de toute définition. C'est comme 'chose'. Ces mots n'ont plus aucun sens, ils deviennent des 'valises' que l'on peut remplir avec les définitions que l'on veut.

Et c'est bien là le problème. Ne signifiant plus rien, c'est tout naturellement que l'on en créé un lorsqu'on veut amalgamer des choses qui ont un vague point commun sur une dimension fonctionnelle importante (le son, les textures, l'affichage ou je ne sais quoi encore) et dont on ne sait pas trop comment articuler les différents éléments. Prends tes classes GestionnaireXXX et remplace par BiduleXXX. Ça marche aussi bien. Car les deux mots sont tous les deux vides sémantiquement et donc disponible pour être rempli par ce que tu souhaites y mettre. Et donc propice à l'inflation continue jusqu'à, grenouille devenant GodClasse, l'architecture explose : nombres d'anomalies, régressions, 'peur' des mainteneurs à intervenir dans ces zones malfamées, incompréhension manifeste du comportement, etc...

J'utilise souvent cette boutade : un "manager" en OOD c'est comme entreprise : ça a une responsabilité mal défini, ça veut tout faire, t'es obligé de passer par lui pour n'importe quelle moindre action, et au final ça ne fait rien correctement, c'est un goulot d'étranglement et ça finit par être court-circuité, surtout dans l'urgence. Si c'est caricatural pour un manager d'entreprise, ça finit par être assez ressemblant à un GestionnaireXXX :)

Je pense qu'il y a un lien direct entre le langage humain, la structuration de la pensée et l'architecture (le code formel) résultant. C'est la fameuse phrase de Boileau :

Ce que l'on conçoit bien s'énonce clairement,
Et les mots pour le dire arrivent aisément.

Que l'on peut prendre à l'envers : dans un projet, les concepts qui ont leur(s) mot(s) juste(s) seront ceux qui seront le plus facilement designer.

Dans la même veine, des noms de classes, de variables, de fonctions indiquent immédiatement qu'un problème se prépare (me souvient de variables nommées 'running' et 'should_stop' fleurant bon les problèmes dans une discussion sur dvp : http://www.developpez.net/forums/d1...)

Bref, merci pour ce billet.

Je pense que c'est un sujet sur lequel il y a beaucoup à dire et qui peut ouvrir beaucoup de discussion/réflexion

P.S. : à un moment tu parles d'inférence, je suis un peu gêné car cela a un sens qui me parait discutable. A un moment, j'ai été tenté par "association" mais ça demande encore à être réfléchi.

2. Le dimanche, octobre 28 2012, 13:20 par Mat

Article très sympas!!

Un petit commentaire néanmoins. Même si on supposait vraie la phrase de Boileau, cette dernière n'étant qu'une simple implication, on ne peut en déduire sa réciproque...
Les seules maximes affirmées par la phrase de Boileau sont:
"Ce que l'on conçoit bien s'énonce clairement" et "Ce qui s'énonce de manière obscure provient d'une mauvaise conception".

En d'autres termes, bien qu'une mauvaise architecture se repère à sa mauvaise utilisation du vocabulaire, on ne peut juger de la bonne conception d'une architecture via le même filtre. Et tout ça, d'après la sacro-sainte hypothèse que la phrase de Boileau est vraie...

Voila voila

Ajouter un commentaire

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

Fil des commentaires de ce billet