23 nov. 2009

Implémentation d'un système de règle pour un jeu de rôle - Cassons la circularité !

Dans l'article précédent, on a vu que l'implémentation d'un système fermé ayant pour put la gestion d'un personnage dans un jeu de rôle modérément complexe avant des conséquences pour le moins inattendues, et notamment le fait que de nombreuses entités se retrouvait dans un état d'interdépendance complexe. Cet article, plus général que le titre ne le montre, essaie de donner quelques pistes pour casser ces dépendances circulaire dans le but de nous permettre la création d'une architecture simple, ouverte et efficace.

Qu'est-ce qu'une dépendance circulaire ?

Bien évidemment, je pose la question alors que je sais que vous connaissez la réponse. Une dépendance circulaire existe à partir du moment ou une classe A dépends d'elle même de manière indirecte, par opposition à une dépendance directe qui (en termes de design) n'est pas dommageable. Cela signifie que A dépends d'une classe B qui elle même dépends de A.

La conséquence logique est que A et B sont fortement couplées. Ce couplage a un impact non désirable sur à la fois le code et l'architecture.

  • Sur l'architecture, ce fort couplage implique que A et B sont nécessairement utilisées ensemble ; ni l'une ni l'autre n'est réutilisable sans l'autre. Si A dépends aussi de plusieurs classes A1, ..., An, alors l'utilisation de B dans un projet nécessite la connaissance de A, A1, ..., An - ce qui n'a pas vraiment de sens. L'architecture est fortement complexifiée du fait même de ce couplage fort.
  • Sur le code, une modification dans l'un des deux classes a de grandes chances d'impliquer une modification dans l'autre classe - de manière plus formelle, A est une responsabilité de B qui est une responsabilité de A. En vertu du principe de responsabilité unique, cela signifie que ni A ni B ne peuvent avoir d'autre responsabilité ; conceptuellement, on voit bien qu'il y a un problème, parce que ça voudrait dire que ni A ni B ne font quoi que ce soit.

Certaines personnes semblent penser que dès lors que deux classes présentent une forte cohésion (c'est à dire qu'elles traitent de sujets très similaires et qu'il y a de fortes chances qu'elles soient utilisées dans le même cadre applicatif) alors le fait qu'elles soient en plus fortement couplées n'a pas véritablement d'incidence sur la qualité du design ou sur le code. Ce n'est pas vrai, pour une simple et bonne raison : tôt ou tard, un couplage fort entraine l'utilisation dans un module de dépendances qui n'ont pas de lien de cohésion avec les autres parties du module. La cohésion globale est donc réduite d'autant. A terme, plus le couplage est fort, plus la cohésion du module sera faible. Seul un couplage faible permet de s'assurer d'une forte cohésion entre les différentes classes d'un module.

Soit ; que faire alors ?

Nous ne connaissons hélas pas beaucoup de manières différentes pour casser ce type de relation malsaine, d'autant que dans notre cas cette relation n'est pas seulement une vue de l'esprit mais bel et bien une réalité liée à l'architecture des règles elle-même.

La première technique est l'ajout de niveaux d'abstraction supplémentaires. Cette technique consiste à découpler deux classes A et B en rajoutant une classe intermédiaire C telle que ("x -> y" signifie "x dépends de y")

  • avant la modification : A1 -> B1 -> A1
  • après la modification : A2 -> B2, A2 -> C2 et B2 -> C2 (généralement, C2 est une super-classe de A2).


Dans la famille "Avant - Après", je veux le diagramme de classe

Cette technique est efficace dans de nombreux cas. Pour notre cas particulier, on s'aperçoit rapidement que cette solution n'est pas envisageable - compte tenu du nombre d'interactions entre les différentes classes. En effet, si il est aisé de casser une dépendance circulaire entre deux classes, il est beaucoup plus complexe avec cette technique de casser la dépendance circulaire entre N classes fortement interdépendantes.

L'autre solution est un peu plus complexe à visualiser dans sa version complète, mais elle convient parfaitement à notre cas : c'est l'application de la métaphore livreur de lait[1]. Le livreur de lait travaille pour le distributeur lactus, connue par tout les producteurs et consommateurs de lait du monde entier ou presque. Lactus prends à sa charge les livraisons de lait partout : un producteur envoie son lait à Lactus, qui l'envoie aux clients qui souhaite boire ce lait spécifique.

Dans ce monde parfait, Lactus n'a pas besoin de connaître la composition exacte de chaque bouteille de lait qui transite par ses locaux. Elle pourrait ne rien connaître du tout, mais ça la forcerait à envoyer le lait à tout le monde si elle souhaite être sûre de n'oublier personne. Afin d'optimiser ses ressources humaines, elle a donc imposé un système simple : chaque bouteille possède une étiquette particulière avec la marque du fabricant. Les consommateurs demandent alors les bouteilles de lait de tel ou tel fabricant.

Les avantages de ce système sont certains : celui qui envoie le lait n'a pas à savoir qui va le recevoir ; celui qui reçoit le lait se moque de qui est vraiment le producteur ; et enfin, Lactus n'a pas besoin de connaître quoi que ce soit à propos du lait échangées ou à propos de ses clients, si ce n'est le strict minimum - la marque du lait et l'adresse du client.

On voit clairement que ce système est fort bien adapté à notre problème : avec une telle vision, il devient possible de regrouper beaucoup d'entités qui autrement serait interdépendantes pour en faire des entités qui sont complètement indépendantes. A noter que vous pourriez croire que si cette technique marche bien dans ce cas, il n'y a pas de raison qu'elle ne marche pas dans le cas simple (A -> B -> A). Il est évident qu'elle fonctionnera - mais à quel prix ? Alors que l'ajout d'une abstraction supplémentaire ne coute quasiment rien, un système de message tel que celui décrit ici nécessite une boite à lettre, des objets messages et tout une ribambelle de petites considérations qui rendent le design non trivial. De fait, il convient de réserver cette solution aux cas ou elle correspond vraiment bien - d'autant que dans la grande majorité des cas, c'est le problème simple qui se pose, et ce problème simple bénéficie grandement de l'ajout d'une abstraction.

A bientôt pour de nouvelles aventures...

Notes

[1] en fait, je l'appelle comme ça, mais elle n'a probablement aucun nom...

Commentaires

1. Le lundi, juillet 5 2010, 13:13 par Ekinox

Même commentaire que sur l'article précédent dans la série des jeux de rôles, les diagrammes auraient beaucoup éclairé mon pauvre esprit borné qui a du mal à comprendre le texte (enfin, faut pas pousser quand même :p )

Ajouter un commentaire

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

Fil des commentaires de ce billet