06 déc. 2009

Implémentation d'un système de règle pour un jeu de rôle - Retour à nos problèmes

Après avoir bien étudié les possibilités qui s'offrent à nous pour éviter de tomber dans le piège terrible des références circulaires, nous pouvons reprendre notre petit bonhomme de chemin et étudier un peu plus les besoin liés à l'implémentation d'un système de règle pour un jeu de rôle, et même commencer à proposer des solutions.

Mise à jour : le code source qui met en oeuvre la technique présentée dans ce billet est disponible. Gardez en mémoire qu'il ne s'agit pour l'instant que d'un début - il est fort possible que l'ensemble évolue très rapidement. Le code source est compilable sous linux (./configure && make && make check) et sous Windows avec Visual Studio 2008 (au moins ; quelques warnings subsistent). Bien évidemment, il s'agit de C++, et l'ensemble du système de gestion de message est basé sur des classes template - de manière à maximiser sa réutilisation future. Amusez vous bien avec ce code source !

Update : contrairement à ce que je dis dans le paragraphe ci-dessus, le code source N'EST PAS compilable sous Windows. Les évolutions que j'ai apporté à celui-ci ne sont pas reportées dans la configuration VS2008, si bien que le projet n'est pas compilable dans cet EDI. Désolé pour ce manque incroyable de tact (je vais tenter de remédier à ce problème le plus vite possible).

Petit rappel

Vous vous rappelez de ce schéma, publié dans le second article de cette série ?

rpg-character-closed-system.png

L'article précédent nous a donné les outils qui vont nous permettre de casser une partie de ces liens et ainsi nous donner la possibilité d'implémenter ce système - et même plus.

Cet article continue de considérer le personnage comme étant un système fermé, de manière à ne pas s'encombrer des difficultés supplémentaires liées à son intégration dans un environnement encore plus complexe que lui. On verra dans les articles suivants que cette approche ne fait que nous donner une base de travail, sans pour autant nous fournir la solution correcte au problème pris dans son ensemble.

Le livreur de lait - du producteur au consommateur

Ceux qui lisent régulièrement ces pages savent qu'il existe des solutions efficaces pour répondre au problème de la transmission de messages. Le sujet a été traité en particulier dans la série Du contrôle des fenêtres, et notamment dans la première partie de cette série. Cependant, la solution mise en œuvre était à ce moment très liée à l'architecture des messages Windows - principalement pour des raisons de simplicité. Nous avons besoin d'un système plus générique, offrant les fonctions suivantes :

  • N'importe qui peut envoyer un message ; le monde des producteurs est ouvert.
  • N'importe quelle classe d'objet peut enregistrer une ou plusieurs fonctions de réception de message ; je ne dois pas demander à une classe de dériver d'une classe spécifique dès lors que je souhaite déclarer une fonction de réception de message. Mon but est de présenter un système générique d'envoi/réception de messages. Idéalement, il devrait même m'être possible de créer une fonction libre (hors classe) et de l'enregistrer en tant que fonction de réception de message. Plus formellement, le monde des consommateurs est ouvert.
  • Plusieurs fonctions de réception de message peuvent réceptionner le même message ; un message n'a pas nécessairement un seul destinataire, car j'ai une relation n/n entre les producteurs et les consommateurs ( ce qui signifie que Np producteurs peuvent envoyer un message particulier, et chaque message peut être traité par Nc consommateurs).

Parmi ces trois points, c'est bien évidemment le second qui est le plus important. En substance, il nous empêche de mettre en place une solution similaire à celle mise en place dans le cadre de la série Du contrôle des fenêtres puisque cette dernière s'appuie principalement sur le fait que tous les objets réceptionnant des messages sont des fenêtres, qui dérivent d'une classe unique window.

Formalisation

Qu'est-ce qu'une fonction de réception de message ? Au niveau formel, il s'agit d'une fonction de rappel donc avant tout d'une entité qu'on peut exécuter.

Définition : FRM, fonction de réception de message
Une fonction de réception de message une fonction de rappel typée, aussi appelée fonction déléguée

Selon les langages, plusieurs possibilités nous sont offertes[1] :

  • une méthode de classe
  • une fonction libre (uniquement dans les langages permettant l'utilisation du paradigme procédural; par exemple, C++)
  • un objet fonction (certains langages orientés objet ne supporte pas la création de foncteurs)
  • une fonction anonyme (uniquement dans les langages permettant l'utilisation du paradigme fonctionnel ; par exemple, C#, Java).[2]
  • une fermeture (uniquement dans les langages supportant cette fonctionnalité ; par exemple C#, Java, JavaScript)[3]

Les FRM traitent des messages.

Définition: fonction reduction et messages
Soit T l'ensemble des types de donnée qu'il est possible de décrire avec un langage L, et K un sous-ensemble de T sur lesquelles l'opérateur == est défini. On définit la fonction reduction de T vers K telle que quelque soit t1, t2 de T, reduction(t1) == reduction(t2) si et seulement si t1 et t2 sont identiques.
Un message est une instance d'un type M de T.

Vous devez vous demander pourquoi est-ce que je parle de fonction reduction - quelle espèce de lien peut-elle avoir avec notre problème ? Si vous vous rappelez de cette histoire de bouteille de lait et d'étiquettes, alors vous ferez sans doute le rapprochement : la fonction reduction n'est rien d'autre que le système d'étiquetage des messages, qui transforme les informations fournies par le producteur (le message lui même) en un code compréhensible par l'entreprise de livraison de lait.

Histoire d'être complet, on termine notre formalisation du problème avec quelques définitions supplémentaires.

Définition : fonction traduction
Soit T l'ensemble des types de données qu'il est possible de décrire avec un langage L. Soit A, M des sous-ensembles de T. On définit une fonction traduction de M vers A, ou M est un type de message et A est le type d'argument d'une FRM.

Pourquoi a-t-on besoin d'une fonction traduction ? Celle-ci n'entre pas vraiment dans le schéma décrit dans la métaphore du livreur de lait. L'utilité de cette fonction est plus avancée, et peut se comprendre en augmentant légèrement la métaphore sus-dite.

Supposons que notre livreur de lait soit non pas une entreprise locale mais une entreprise mondiale. Elle reçoit des bouteilles de lait en provenance de producteurs du monde entier, et les distribue à ces clients qui peuvent être située à des milliers de kilomètres de l'usine du producteur. Chaque bouteille de lait présente des informations utiles au livreur (qui les exploite après passage dans la fonction reduction) et des informations qui sont utiles au consommateur. Le consommateur est (comme tout consommateur qui se respecte) un peu tatillon, et il s'attend à récupérer ces informations sous une certaine forme. Pour notre producteur, c'est un problème, car ne sachant pas quel client va recevoir la bouteille, il ne peut pas anticiper la façon dont les informations doivent être arrangée. Le livreur passe donc par des petites entreprises locales, sises près des consommateurs, et dont le but et de transformer les informations figurant sur les bouteilles de lait dans le format attendu par les consommateurs. Ainsi, ces derniers ont accès aux informations dont ils ont besoin, sans pour autant que ça n'ai d'impact sur le travail du producteur et du livreur. C'est le rôle de la fonction traduction.

C'est bien beau tout ça, mais concrètement...

La section précédente nous a donné des informations sur les opérations que nous devons supporté, mais ne nous a pas vraiment aidé à concevoir notre système de gestion de messages.

Quels sont les collaborations que nous allons devoir prendre en compte ? Les extrémités du processus sont évidentes : nous allons avoir une entité qui va envoyer le message (le producteur) sous une forme connue F1, une entité qui va réceptionner le message (le consommateur) sous une forme F2. Entre temps, le message au format F1 aura été traité par le livreur grâce à la fonction reduction, et un prestataire l'aura transformé grâce à la fonction traduction. Le producteur n'est pas vraiment partie prenante du système - il en est l'utilisateur. Nous le laissons donc de coté. De même, le consommateur est lui aussi un utilisateur.

Dans notre architecture, nous allons donc déclarer les classes suivantes :

  • message, notre type M.
  • key, le type K de retour de la fonction reduction
  • message_dispatcher, l'équivalent de notre livreur de lait (connait les types M, K et R, type de retour des FRM)
  • marshaller, l'équivalent de notre prestataire local (connait les types M, A - type de retour de la fonction traduction - et R)
  • delegate, qui encapsule une FRM (connait A et R)

En temps normal, les opérations suivantes prennent place :

  • Les consommateurs enregistrent les FRM (sous la forme d'un delegate associée à une valeur de type key et à un marshaller) auprès de l'instance de message_dispatcher.
  • Lorsqu'un producteur envoie un message au message_dispatcher, ce dernier le transforme en instance de key de manière à récupérer la liste des FRM destinataires du message.
  • Le message est transformé par le marshaller associé au delegate en un argument d'un type connu par ce dernier.
  • Le delegate est alors exécuté et appelle le FRM.

Selon le langage de programmation utilisé, quelques complications peuvent arriver lorsqu'on souhaite effectuer des opérations qui paraissent triviales mais ne le sont pas (notamment, en C++, le fait de désinscrire un FRM - il nous faut alors trouver le moyen de retrouver ce FRM particulier dans la liste des FRM enregistrés. Je vous laisse gérer ce point particulier en forme d'exercice).

Pour finir, je vous livre le diagramme de classe correspondant à cette architecture. Le code source C++ documenté lié à ce projet devrait faire l'objet d'une annonce d'ici le début de la semaine prochaine.

message_dispatcher.png

A bientôt !

Notes

[1] Je ne vous cache pas que malgré cette envie dévorante de vous présenter un code source adaptés à vos besoins, je ne vais pas (dans cet article et dans les suivants) vous livrer de fichier écrits dans un autre langage que le C++ - par goût pour ce langage imperméable, et parce que je dois bien faire un choix. Je proposerai toutefois de temps en temps des exemple de code dans d'autres langages.

[2] La prochaine version de C++ supporte les fonctions anonymes.

[3] La prochaine version de C++ supporte les fermetures.

Commentaires

1. Le mercredi, mars 24 2010, 11:29 par Arzar

Dommage que le projet Visual Studio livré avec le billet soit complètement en vrac.
Entre les fichiers qui ne pointent pas au bon endroit, les fichiers qui n'existent pas du tout et la référence à un dossier "Include" inexistant, j'ai fini par abandonner :/

2. Le mercredi, mars 24 2010, 16:08 par Emmanuel Deloget

Mea culpa - le code source est pour l'instant complètement expérimental, et ne compile en fait que sous Linux avec cppunit installé (pour les tests unitaires), contrairement à ce qui est annoncé dans le billet (que je mets à jour tout de suite...).

La version 0.3 en cours de développement compilera sous Linux et sous Windows (VS2008, voire VS2010 > RC)

Encore une fois, mea culpa, mean maxima culpa.

Ajouter un commentaire

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

Fil des commentaires de ce billet