Blog 7 min de lecture

Apprenez les design patterns avec le projet de jeu Rigs of Rods

Share this article
Apprenez les design patterns avec le projet de jeu Rigs of Rods

La majorité des développeurs ont déjà entendu parler des design patterns ; les patterns du GOF (Gang Of Four) sont les plus popularisés, et chaque développeur a sa propre façon de les apprendre. On peut citer :

  • La lecture d'un livre.
  • Des sites web.
  • Un collègue.
  • Une formation.

Quelle que soit la méthode choisie, on peut apprendre les patterns par cœur et passer des heures à mémoriser leurs diagrammes UML, mais les appliquer à un projet réel peut s'avérer plus difficile.

L'essentiel n'est pas de mémoriser les noms exacts des patterns ni de les implémenter exactement comme décrit dans la documentation ; ce qui compte le plus, c'est de comprendre la motivation derrière chaque pattern — les patterns naissent de ces motivations.

Une bonne façon de mieux comprendre les motivations derrière ces patterns est de les étudier dans un projet réel. C'est l'objectif de cet article : nous allons explorer le code source d'un projet open source qui en fait un usage extensif.

Analyse de Rigs of Rods

Rigs of Rods (« RoR ») est un jeu de multi-simulation open source qui utilise la physique des corps mous (soft-body) pour simuler le mouvement et la déformation des véhicules. Le jeu est construit sur un moteur physique de corps mous spécifique appelé Beam, qui simule un réseau de nœuds interconnectés (formant le châssis et les roues) et permet de simuler des objets déformables. Avec ce moteur, les véhicules et leurs charges fléchissent et se déforment lorsque des contraintes sont appliquées. Percuter des murs ou le terrain peut déformer un véhicule de façon permanente.

Explorons quelques-uns des design patterns du GoF utilisés par RoR.

Singleton

Le Singleton est l'un des design patterns les plus populaires et les plus utilisés. RoR utilise un singleton générique pour éviter de répéter le même code pour chaque classe singleton. Il définit deux variantes : un singleton qui crée une nouvelle instance et un autre où une instance déjà créée est assignée.

Recherchons tous les singletons de RoR ; pour cela, nous pouvons utiliser CQLinq:

from t in Types where t.DeriveFrom(“RoRSingletonNoCreation“) || t.DeriveFrom(“RoRSingleton“)
select t
Motivation :

Prenons l'exemple du singleton InputEngine : RoR doit stocker des données sur le clavier, la souris et les joysticks, qui sont détectés à l'initialisation par la classe InputEngine. De nombreuses classes ont besoin des mêmes données de périphériques d'entrée, et il n'y a pas besoin de créer plus d'une instance, donc la motivation principale est de « Créer une seule instance de la classe InputEngine ».

Cependant, l'utilisation du singleton est devenue controversée, et tous les architectes et concepteurs ne le recommandent pas ; voici un article sur la controverse du singleton.

Factory Method

Il n'y a pas de mystère derrière les factories ; leur objectif est simple : créer des instances. Une simple factory contenant une méthode CreateInstance pourrait atteindre cet objectif. Cependant, RoR utilise le pattern Factory Method pour toutes ses factories au lieu d'une simple factory.

Motivation :

Pour mieux comprendre ce pattern, regardons un scénario dans lequel RoR l'utilise :

  • RoR utilise le moteur graphique OGRE, qui doit instancier des classes de type ParticleEmitter.
  • RoR définit sa propre classe ParticleEmitter, nommée BoxEmitter, qui hérite de ParticleEmitter, et il a besoin qu'OGRE utilise cette nouvelle classe comme un ParticleEmitter.
  • OGRE ne sait rien de RoR.

La question est : comment OGRE saura-t-il instancier et utiliser cette nouvelle classe BoxEmitter de RoR ? C'est là qu'intervient le pattern « Factory Method » :

OGRE possède une classe abstraite nommée ParticleEmitterFactory qui fournit la méthode CreateEmitter. Pour faire son travail, OGRE a besoin d'une factory concrète. RoR définit une nouvelle factory, BoxEmitterFactory, héritant de ParticleEmitterFactory, et redéfinit la méthode CreateEmitter.

RoR fournit cette factory à OGRE via ParticleSystemManager::addEmitterFactory(ParticleEmitterFactory *factory). Chaque fois qu'OGRE a besoin d'une instance de ParticleEmitter, BoxEmitterFactory est invoquée pour la créer.

La motivation la plus importante est le faible couplage; en effet, OGRE ne sait rien de RoR et pourtant il peut instancier des classes de ce dernier.

Une autre motivation est de renforcer la cohésion en déléguant l'instanciation à une classe factory spécifique.

Utiliser une simple factory est utile pour isoler la logique d'instanciation et améliorer la cohésion, mais le pattern « Factory Method » est mieux adapté lorsqu'un faible couplage est également requis.

Template Method

La template method définit le squelette d'un algorithme dans une méthode, en déléguant certaines étapes aux sous-classes. La template method permet aux sous-classes de redéfinir certaines étapes d'un algorithme sans changer la structure de celui-ci.

L'objectif est de garantir que la structure de l'algorithme reste inchangée tandis que les sous-classes fournissent des parties de l'implémentation.

Utilisons CQLinq pour détecter toutes les classes utilisant le pattern template method. Pour cela, nous pouvons rechercher les classes abstraites (la classe Abstract du diagramme UML ci-dessous) ayant une ou plusieurs méthodes (templateMethod() du diagramme) qui utilisent des méthodes implémentées dans la sous-classe (primitive1 et primitive2 du diagramme).

from t in Types where t.IsAbstract && t.Methods.Where(a=> a.NbLinesOfCode>0 && a.MethodsCalled.Where(b=>b.IsPureVirtual && b.ParentType==t).Count()>0).Count()>0 select t
Motivation :

Prenons la classe IRCWrapper comme exemple. Sa méthode « process » contient la logique de traitement des événements IRC reçus. Voici les méthodes qu'elle appelle :

Elle invoque la méthode virtuelle pure processIRCEvent, qui doit être implémentée par une classe dérivée d'IRCWrapper. LobbyGui est une telle classe. Elle doit traiter les événements IRC reçus, donc elle redéfinit la méthode processIRCEvent pour implémenter son comportement spécifique.

Avec ce pattern, nous pouvons facilement changer l'implémentation d'un algorithme sans changer son squelette. Il réduit le code boilerplate et rend ces classes plus faciles à maintenir.

Il renforce aussi le faible couplage, car le client peut ne référencer que la classe abstraite au lieu des classes concrètes.

Strategy

Il existe de nombreuses situations dans lesquelles des classes ne diffèrent que par leur comportement. Dans de tels cas, c'est une bonne idée d'isoler les algorithmes dans des classes séparées afin de pouvoir sélectionner différents algorithmes à l'exécution.

Utilisons CQLinq pour détecter toutes les classes utilisant le pattern strategy. Pour cela, nous pouvons rechercher les classes abstraites ayant plusieurs classes dérivées, où le client référence la classe abstraite au lieu des implémentations concrètes.

from t in Types where t.IsAbstract && t.DirectDerivedTypes.Count()>1 !t.IsThirdParty
let tt=t.DirectDerivedTypes
from db in tt where db.Methods.Where(a=>a.NbMethodsCallingMe!=0 !a.IsStatic).Count()==0
select new {db,t}
Motivation :

La caméra peut avoir plusieurs comportements — fixe, libre, statique ou isométrique — et son comportement peut être changé dynamiquement. Des comportements supplémentaires peuvent aussi être ajoutés à l'avenir.

CameraManager utilise la classe abstraite IBehavior. Voici toutes les méthodes de CameraManager qui utilisent IBehavior.

Comme on peut le voir, il y a une méthode nommée switchBehavior qui change le comportement dynamiquement.

Ce pattern renforce le faible couplage — en effet, CameraManager ne connaît pas les comportements concrets — et renforce aussi la forte cohésion, car chaque comportement spécifique est implémenté dans une classe isolée.

State

Le pattern State est similaire au pattern Strategy d'un point de vue architectural, et c'est pourquoi, avec la requête CQLinq précédente où nous recherchions le pattern strategy, nous avons aussi trouvé des classes d'états.

Cependant, leurs objectifs sont différents : le pattern Strategy représente un algorithme qui utilise une ou plusieurs implémentations d'IStrategy. Il n'y a pas de corrélation entre ces différents comportements ; en revanche, avec le pattern State, on transite d'un état à un autre pour atteindre l'objectif final, il y a donc une relation entre les différents états.

Voici toutes les classes d'états héritant de la classe abstraite AppState.

Comme avec le pattern Strategy, les autres classes ne référencent que la classe abstraite. Voici toutes les méthodes qui utilisent AppState.

Comme on peut le voir, AppStateManager contient plusieurs méthodes pour gérer le cycle de vie des états.

Motivation :

Comme le pattern Strategy, ce pattern renforce le faible couplage — AppStateManager ne connaît pas les états concrets — et renforce aussi la forte cohésion, car chaque opération est isolée dans son état correspondant.

Façade

Une façade est un objet qui fournit une interface simplifiée à un ensemble de code plus vaste, comme une bibliothèque de classes. Une façon simple d'identifier les façades utilisées est de rechercher le code externe référencé par le projet.

Voici tous les espaces de noms utilisés par le projet RoR :

Prenons l'espace de noms Caelum comme exemple et recherchons les classes de RoR qui l'utilisent.

from m in Methods where m.IsUsing (“Caelum“)
select new { m }

Seul SkyManager utilise directement l'espace de noms Caelum, il représente donc la façade Caelum.

Motivation

Si nous utilisons une bibliothèque externe fortement couplée à notre code — c'est-à-dire que de nombreuses classes utilisent directement la bibliothèque — il sera très difficile de la remplacer. En revanche, si une façade est utilisée, seule son implémentation devra changer si nous voulons remplacer la bibliothèque externe.

Ce pattern renforce le faible couplage avec les bibliothèques externes.

Conclusion

Après avoir appris les patterns du GoF, il est utile de comprendre les motivations pour les utiliser dans votre code. Explorer comment les patterns sont implémentés dans des projets open source bien connus peut vous aider à mieux comprendre leur valeur.

Share this article