Souplesse et modularité grâce aux Design Patterns


précédentsommairesuivant

1. Patterns de programmation modulaire

Cette partie passe en revue 3 patterns importants: Composite, Decorator et Strategy, qui permettent la mise en place de composants modulaires (et constituent le point de départ de notre réflexion sur l'approche modulaire de la programmation).

1.1. Composite

Le pattern Composite permet la création de hiérarchies d'objets. Il est à la base d'autres patterns tels que Décorateur.

Un objet est dit "Composite" s'il est composé d'autres objets appartenant à la même hiérarchie que lui (et qui peuvent être eux aussi des objets composites).

Voici le diagramme de classes correspondant au pattern Composite :

Composite
Le pattern Composite



L'intérêt du pattern Composite est de pouvoir créer des objets complexes par assemblage d'objets simples (dits "atomiques"). La hiérarchie ainsi obtenue est une structure arborescente.



Imaginez que vous ayez à modéliser une scène en 3D. Les plus petits éléments de cette scène seront sans doute des triangles qui sont à la base de formes plus complexes telles que des pyramides, des cubes, des sphères, des tores, etc. Une fois combinées, ces formes constitueront par exemple un doigt, une main, un bras, un corps, etc. C'est à cela que sert le pattern Composite.

Nous verrons plus tard que le pattern Composite s'associe très bien avec d'autres patterns tels que Visitor ou Prototype pour, respectivement, ajouter des traitements ou permettre la copie d'objets.

1.2. Decorator

Le pattern Decorator permet de modifier dynamiquement les comportements de certains objets. Il permet également d'ajouter de nouvelles fonctionnalités par combinaison de modules existants.

Le pattern Decorator fonctionne sur le principe des LEGO(R): on créé de nouveaux comportements en assemblant des modules, qui constituent les "briques" de notre application:

Principe des Décorateurs
Décorateurs: Principe



On comprend alors l'intéret de cette méthode: c'est extrêmement modulable et extensible quasiment à l'infini sans avoir recours à l'héritage.



Prenons l'exemple d'un Jeu de Rôles. Dans un JDR, on trouve toutes sortes d'armes classées en catégories (épées, arcs, arbalètes, lances, masses...) avec des caractéristiques différentes. L'un des intérêts du jeu de rôle réside dans la profusion de ces artéfacts (la recherche d'armes de plus en plus puissantes donne souvent lieu à de nombreuses quêtes qui allongent la durée de vie du jeu et prolonge le plaisir éprouvé par le joueur dans la découverte du jeu).

Parallèlement, de nombreux sorts peuvent modifier les caractéristiques d'une arme (sort de feu, de glace, de poison, de rapidité, etc.). Dès lors on comprend bien qu'il est inconcevable de déclarer autant de classes qu'il existe de combinaisons arme/sort possibles (ex: EpeeCourteEnflammee, EpeeCourteEmpoisonnee, etc.). Une solution possible pour résoudre ce problème est l'usage du pattern Decorator.

Voici le diagramme de classes correspondant au pattern Decorator :

Décorateur: Diagramme de classes
Décorateur: Diagramme de classes



Dans notre exemple, les objets concrets correspondent aux armes (épées, arcs, etc.) et les différents sorts modifiant leurs caractéristiques sont des Décorateurs. Ainsi, une épée courte enflammée est obtenue par simple composition de l'objet EpeeCourte avec le décorateur SortFlamme (qui augmente le nombre de dommages infligés par l'épée).



Les Décorateurs sont également très utiles pour définir des chaînes de traitements. Imaginez que vous ayez une application qui doit appliquer N traitements à X enregistrements. La plupart des développeurs auront tendance à procéder de la sorte :

Exemple à ne pas reproduire
Sélectionnez
Pour chaque enregistrement
  Pour chaque traitement
    enregistrementCourant := traitement(enregistrementCourant)
  FinPour
FinPour

Ceci est extrêment coûteux en termes de mémoire, et aussi en termes de temps lorsqu'il faut charger chacun des enregistrements en mémoire avant traitement. Avec le pattern Decorateur, on procède différemment: on définit une chaîne de traitements dans laquelle le résultat du traitement courant est immédiatement utilisé par le traitement suivant. A la fin de la chaîne, on obtient l'enregistrement complètement traité.

Cette manière de faire permet d'économiser énormément de mémoire et de temps (les objets sont traités sous forme de flux), mais l'idée essentielle est de pouvoir définir un traitement complexe comme un assemblage de traitements simples, offrant ainsi souplesse et modularité.

On retrouve un principe similaire dans le shell Unix (sauf qu'ici il n'est pas question de POO) avec l'assemblage de commandes simples au moyen de "tubes" ("pipes" en Anglais), le résultat en sortie d'une commande se trouvant utilisé en entrée standard d'un autre.

1.3. Strategy

Le pattern Strategy permet de définir des comportements (ou des algorithmes) interchangeables.

Prenons un exemple: Imaginons que vous vouliez écrire un jeu vidéo de courses automobiles. Chaque véhicule est composé de différents éléments (moteurs, pneus, freins, suspensions, turbo...). Chaque élément influera sur le comportement du véhicule (vitesse, accélération, tenue de route...) et au fil du jeu, on aura accès à de nouveaux composants qui donneront la possibilité d'améliorer notre véhicule (en mettant un moteur plus puissant, en ajoutant un turbo plus performant, etc.).

Dans cet exemple, chacun des composants du véhicule est interchangeable (on peut changer une pièce d'un certain type par une autre pièce de même type), ce qui conférera au véhicule un comportement différent.

Stratégie: jeu de course automobile
Stratégie: jeu de course automobile



Autre exemple: pour un jeu vidéo de stratégie temps réel, on veut développer un générateur de cartes. La génération de la carte comprend différentes étapes: génération du relief, des textures de terrain, de la végétation, des ressources souterraines, d'implantation des villes... Pour multiplier les combinaisons possibles, on décide que pour chacune de ces étapes on aura des algorithmes de distribution différents et interchangeables. Ici aussi on a recours à une stratégie:

Stratégie: générateur de niveaux
Stratégie: générateur de niveaux



Dans les deux exemples cités plus haut, on a:

  • un objet client, qui a besoin d'algorithmes interchangeables
  • une hiérarchie d'objets Strategy, qui permet d'obtenir des algorithmes interchangeables



Dans certains cas, il est possible d'utiliser une Strategie à la place d'un Décorateur. Ce qui diffère alors c'est que le composant a connaissance des stratégies qu'il utilise (une arme aurait connaissance des sorts qui lui sont appliqués), alors qu'un Décorateur modifie de manière externe le comportement du composant :

Décorateur contre Stratégie
Décorateur contre Stratégie



Liens externes:

Génération de texture, par fearyourself
Génération de terrain, par khayyam90


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2006 Pierre Caboche. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.