Souplesse et modularité grâce aux Design Patterns

Pierre Caboche

Cet article montre comment il est possible de créer des programmes souples et puissants au moyen de modules (des éléments interchangeables, réutilisables et combinables entre eux). On y présente de nombreux patterns importants ainsi que la manière de les utiliser et de les combiner entre eux.

Publics visés

- Débutants
Cet article constitue une bonne introduction aux Design Patterns puisqu'on y présente certains Patterns fréquemment utilisés ainsi que la manière dont ils agissent les uns avec les autres.

- Intermédiaires/avancés
Cet article n'est pas un catalogue de Patterns. On se concentre sur les choix de Patterns et leur interaction plutôt que sur l'aspect purement descriptif. Le but est d'approfondir les bases et d'assembler les morceaux d'un puzzle dans le but d'obtenir un tout cohérent.

- Titre : Souplesse et modularité grâce aux Design Patterns

- Auteur : Pierre Caboche

- Parution : 28 août 2006

- Mise à jour : 16 juin 2020

- Licence : 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.

Sommaire

- Introduction

- Les Design Patterns, c'est quoi ?

- Les modules, c'est quoi ?

- I. Patterns de programmation modulaire

- I-A. Composite

- I-B. Decorator

- I-C. Strategy

- II. Patterns connexes

- III. Création des objets

- III-A. Factory Method

- III-B. Prototype

- III-C. Builder

- IV. Ajout de traitements

- IV-A. Visitor

- V. Gestion des objets

- V-A. Proxy

- V-B. Facade / Mediator

- VI. Mise en œuvre

- VI-A. Séparation Instanciation/Données/Traitements

- VI-B. Aspect modulaire des Design Patterns

- Conclusion

- Liens externes

- Remerciements

- Téléchargement

Introduction

Lorsque l'on parle de Programmation Orientée-Objets (ou POO), on pense souvent aux principes d'héritage, de surcharge et de polymorphisme. Ce sont certes les principes inhérents à la POO, cependant un tel modèle a ses limites. En effet, avec ces principes seuls, l'ajout de nouveaux comportements passe obligatoirement par la déclaration de sous-classes et la surcharge des méthodes existantes.

Ce que proposent les Design Patterns décrits dans cet article est tout autre: obtenir de nouveaux comportements par combinaison d'éléments existants et interchangeables plutôt que par simple héritage. C'est cela qui constitue l'approche modulaire de la programmation. Une telle approche se révèle à la fois très souple et très puissante, les possibilités d'extension étant quasiment illimitées.

Cet article a pour but de montrer les avantages de l'approche modulaire (ou combinatoire) par rapport à l'héritage simple. On présentera alors plusieurs Design Patterns (parmi lesquels quelques incontournables). Pour agrémenter notre propos, les exemples choisis sont inspirés de l'univers du jeu vidéo (ex. : armes et sorts d'un jeu de rôle, véhicules d'un jeu de courses, générateur de niveau pour un RTS, etc.).

La partie 1 présente les patterns qui sont à la base de l'approche modulaire de la programmation. La partie 2 présente les patterns qui facilitent la mise en œuvre de cette approche modulaire (création et gestion d'objets, gestion mémoire, etc.). La partie 3 est une discussion plus générale sur les Design Patterns et l'approche modulaire.

Mais avant cela, quelques explications s'imposent.

Les Design Patterns, c'est quoi ?

Les Design Patterns fonctionnent sur le principe suivant : à un problème donné, on apporte une solution précise et exposant à la fois le principe, la mise en œuvre, les avantages, les inconvénients et des exemples d'application. Ainsi, la solution proposée est à la fois standard, réutilisable et combinable avec d'autres solutions (bien souvent complémentaires).

Les Design Patterns ont beaucoup de points communs avec le sexe :

1. Lorsqu'on les utilise pour la première fois, on fait souvent des erreurs ;

2. Au début, il faut s'y reprendre à plusieurs fois avant d'obtenir un résultat correct ;

3. Une fois qu'on y a goûté, on ne peut plus s'en passer ;

4. Pour faciliter l'apprentissage, il est conseillé de pratiquer avec une personne expérimentée ;

5. Les maîtriser demande pas mal d'expérience ;

6. Chacun est libre de trouver des techniques personnelles qu'il pourra améliorer avec le temps (avant d'en faire profiter les autres…).

Justement, j'ai envie de vous faire profiter de ma connaissance des Design Patterns. Aussi nous allons voir comment les patterns vont nous aider à concevoir des applications modulables.

Les modules, c'est quoi ?

La définition de modules est double.

Définition 1
Les modules sont des éléments interchangeables, réutilisables et combinables entre eux. Ces éléments réutilisables peuvent prendre des formes diverses: classes, programmes exécutables, scripts, code source… Nous ne nous intéresserons qu'aux classes.

Définition 2
Les modules permettent la séparation des différentes composantes de l'application (ex. : séparation données/traitements/affichage) afin de rendre leur développement indépendant les uns des autres. Ici, la notion de « module » rejoint la notion de « package ».

Ces deux aspects seront abordés dans l'article.

I - Patterns de programmation modulaire

Cette partie passe en revue trois 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).

I-A - 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 :

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.

I-B - 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ée de nouveaux comportements en assemblant des modules, qui constituent les « briques » de notre application :

Décorateurs: Principe

On comprend alors l'intérêt 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

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

Pour chaque enregistrement
Pour chaque traitement
enregistrementCourant := traitement(enregistrementCourant)
FinPour
FinPour

Ceci est extrêmement 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. À 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.

I-C - 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

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

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

Liens externes

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

II - Patterns connexes

Dans la section précédente, nous avons vu comment créer des objets modulaires permettant d'introduire de nouveaux comportements par combinaison plutôt que par héritage. Maintenant, plusieurs questions se posent :

- comment créer ces objets ?

- comment gérer les objets créés ?

- comment ajouter de nouvelles fonctionnalités sans devoir tout réécrire ?

Pour répondre à ces questions, nous avons à notre disposition plusieurs patterns qui vont nous faciliter le travail. C'est ce que nous verrons dans cette section.

III - Création des objets

Tout d'abord, nous allons nous intéresser aux patterns permettant la création d'objets. En effet, la difficulté réside dans le fait d'assembler des objets de base (dit « atomiques ») pour obtenir des objets complexes. Pour cela, on dispose le plus souvent d'objets spécialisés dans l'assemblage et la construction.

III-A - Factory Method

Le pattern Factory Method permet de dissocier la classe de l'instanciation des objets de cette classe. Cette façon de faire peut se révéler plus souple que l'utilisation de constructeurs « classiques », notamment dans le cas d'objets complexes (Composites en particulier).

On se retrouve donc avec deux hiérarchies : d'une part, la hiérarchie des classes porteuses de données et d'autre part, la hiérarchie des « fabriques » (Factory) permettant d'en instancier les objets.

Factory Method

Le pattern Factory Method définit une interface pour la création d'objets (ou groupes d'objets). Ce sont les sous-classes qui implémentent la manière dont ces objets sont instanciés.

III-B - Prototype

Le pattern Prototype consiste à créer de nouveaux objets à partir d'objets existants, par simple recopie. Cela permet ainsi d'élaborer de nouveaux prototypes qui pourront être réutilisés plus tard.

Le principe du pattern Prototype est très simple: une méthode clone() permet de faire une copie conforme d'un objet. Il est alors possible de modifier cette copie afin de créer un objet différent. Cette copie peut ensuite être sauvegardée afin d'être utilisée plus tard ou copiée à son tour.

Le pattern Prototype

Afin de gérer les différents Prototypes, on peut utiliser un gestionnaire de Prototypes qui permet de stocker les prototypes, les classer, les enregistrer, les retrouver en se basant sur leurs identifiants, etc. (cette étape n'est cependant pas obligatoire).

Dans un éditeur de niveaux pour un jeu vidéo, on peut imaginer que l'on dispose de formes de base (implémentées dans le moteur de jeu). Avec ces formes on créé des objets plus complexes que l'on pourra mettre en mémoire, dupliquer (grâce au pattern Prototype), modifier et sauvegarder dans des fichiers afin de les réutiliser plus tard. D'où l'intérêt du pattern Prototype et d'un gestionnaire de prototypes.

Le pattern Prototype se marie très bien avec le pattern Composite, par exemple.

III-C - Builder

Les Builders fournissent une interface simple permettant la construction d'objets complexes avant de retourner l'objet complètement construit à la fin. De cette manière, il est possible de définir une interface commune pour des processus de construction différents.

Un objet de type Builder comporte au moins :

- des méthodes pour créer chaque élément de l'objet en construction ;

- une méthode pour retourner l'objet une fois terminé.

Le pattern Builder

Dans le livre de Gamma et al., les auteurs prennent comme exemple un constructeur de labyrinthe. Le labyrinthe est construit pièce par pièce au moyen de méthodes comme makeRoom et makeDoor. En se basant sur la même interface, ils créent ensuite un Builder qui ne construit pas l'objet réel, mais ne fait que compter le nombre de pièces et de portes qui auraient été créées.

Outre le fait de pouvoir définir des processus de construction différents avec une interface commune (et avoir ainsi des Builder interchangeables), l'intérêt d'un Builder est de pouvoir étendre les processus de construction, par exemple en vérifiant que l'objet en cours de construction répond bien à certaines contraintes (et pouvoir, au besoin, lever une exception).

Un objet de type Builder ne s'utilise jamais seul. Les Builders ne font que définir une interface (pour la classe abstraite) et implémenter la manière dont les différents éléments sont construits (pour les classes concrètes). Les méthodes créant les différents éléments de l'objet ont besoin d'être appelées de l'extérieur (par un parseur, une interface homme-machine, un tout autre objet).

IV - Ajout de traitements

Dans cette section, nous allons nous intéresser à un pattern particulier: le pattern Visitor, qui permet de séparer de manière très simple les données et les traitements associés.

IV-A - Visitor

Le pattern Visiteur permet une séparation précise entre données et traitements. Il est le compagnon idéal du pattern Composite.

Avec le pattern Visiteur, on obtient deux hiérarchies distinctes :

- la hiérarchie des objets support de données ;

- la hiérarchie des Visiteurs (contenant les traitements sur les données).

Le pattern Visiteur

D'un point de vue implémentation, cela nous donne :

Code d'un objet visité

void ObjetDeTypeA::accept( Visitor * v ) {
v->visitObjetDeTypeA( this ) ;
}

Implémentation d'un visiteur

void MonVisiteur::visitObjetDeTypeA( ObjetDeTypeA * objet ) {
// Traitement d'un objet de type A

}

void MonVisiteur::visitObjetDeTypeB( ObjetDeTypeB * objet ) {
// Traitement d'un objet de type B

}

void MonVisiteur::visitObjetDeTypeC( ObjetDeTypeC * objet ) {
// Traitement d'un objet de type C

}

Pour appliquer un visiteur, on procède de la sorte :

Application d'un visiteur

unObjet->accept( unVisiteur ) ;

Voici alors ce qu'il se passe (admettons que unObjet soit de type ObjetDeTypeA) :

1. La méthode accept de ObjetDeTypeA est appelée ;

2. La méthode visitObjetDeTypeA du visiteur v est alors appelée avec comme paramètre this (notre objet de type ObjetDeTypeA) ;

3. On entre alors dans le corps de la méthode visitObjetDeTypeA du visiteur. Le paramètre obj fait référence à l'objet de type ObjetDeTypeA sur lequel on applique notre traitement.

Intérêt du pattern Visitor

Pour ajouter un traitement à notre application, il suffit donc de créer un nouveau Visiteur avant de surcharger les méthodes permettant de visiter les différents objets de la hiérarchie. Ensuite, pour utiliser le visiteur, on fait :

Application d'un visiteur

monObjet->accept( monVisiteur ) ;

De cette manière, on peut facilement ajouter de nouveaux traitements sans toucher à la hiérarchie de nos objets (en POO « classique », on aurait implémenté de nouvelles méthodes pour ajouter de nouvelles fonctionnalités). Grâce aux Visiteurs :

- le code est plus clair (des fonctionnalités différentes se trouvent dans des Visiteurs différents) ;

- des équipes différentes peuvent travailler sur des fonctionnalités différentes sans gêner les autres équipes ;

- on n'est pas obligé de tout recompiler à chaque ajout d'une fonctionnalité (seul le code du Visiteur est recompilé).

Exemple d'application

Imaginons que nous ayons une scène 3D, représentée à l'aide du pattern Composite. On peut vouloir choisir plusieurs modes d'affichage pour cette scène 3D : aperçu en fils de fer, non texturé, texturé et avec différents effets (bump mapping, antialiasing…), ou photo-réaliste au moyen de lancés de rayons. Pour cela, on met en place des Visiteurs spécifiques à chaque mode d'affichage. Cela permet de structurer le programme et de pouvoir facilement ajouter de nouvelles fonctionnalités (ex.: calculer la complexité d'une scène) par simple ajout de Visiteurs.

V - Gestion des objets

Pour terminer sur la présentation des patterns, nous allons nous intéresser aux patterns permettant la gestion des objets en mémoire ainsi que la coordination entre les différents composants du système.

V-A - Proxy

Le pattern Proxy offre une interface simple pour représenter des objets distants (c'est-à-dire pas directement en mémoire: sur le disque dur, dans une base de données, sur Internet, etc.). Le pattern Proxy peut faciliter la gestion des objets en mémoire.

Le plus simple pour comprendre le principe du pattern Proxy est de prendre un exemple. Lorsque vous surfez sur Internet, votre navigateur charge la page web que vous êtes en train de lire. Cette page web fait surement référence à d'autres objets, par exemple des images. Le navigateur commence à afficher la page tout en téléchargeant les différentes images. Les images qui ne sont pas encore chargées sont représentées par un carré d'une certaine taille avec éventuellement un texte alternatif. Voici une utilisation possible d'un objet proxy: donner une représentation d'un objet distant ou trop volumineux pour être mis directement en mémoire.

Ceci est une utilisation possible du pattern Proxy, il en existe plusieurs :

- Gestion des objets distants (sur disque dur, base de données, Internet…) :

- Gestion des objets volumineux, avec création à la demande à partir de ressources distantes :

- Gestion des droits d'accès : restreindre l'accès de certains objets à des utilisateurs particuliers :

- Gestion de la mémoire avec mise en cache des objets les plus souvent accédés, libération de mémoire à la demande, comptage du nombre d'instances en mémoire, etc. Plus généralement, le pattern Proxy s'applique à tous types de smart pointers.

Voici le diagramme de classes correspondant au pattern Proxy :

Le pattern Proxy

Lorsqu'on essaye d'accéder à la méthode methode du Proxy, celui-ci vérifie si l'objet (réel !) est accessible (chargé, présent en mémoire, droits d'accès suffisants). Si c'est le cas, le Proxy appelle la méthode methode de l'objet réel. Sinon, le Proxy renvoie une représentation intermédiaire de l'objet (« en cours de chargement », « accès refusé », etc.).

Dans le cas d'un éditeur de scènes 3D, on peut imaginer que l'outil nous permette de sauvegarder des objets afin de les réutiliser plus tard dans une autre scène. Ces objets seront alors sauvegardés dans des fichiers. Afin d'éviter de devoir charger l'intégralité de ces fichiers au démarrage de l'application (trop lourd, trop long), on peut créer des objets Proxy faisant référence aux différents fichiers.

Ce n'est qu'une fois l'application lancée que les objets iront chercher à la demande (c'est-à-dire lors de l'affichage dans le menu) les informations relatives aux différents objets dans les fichiers correspondants (avec bien entendu la mise en cache de ces informations et libération dynamique de la mémoire).

De même, si certains de ces objets sont trop volumineux pour être redessinés à chaque fois, le Proxy peut servir à en donner une représentation intermédiaire, moins détaillée.

V-B - Facade / Mediator

Le pattern Facade fournit une interface unique pour accéder à l'ensemble des composants d'un sous-système.

Pour la gestion de nos objets, on a eu recours à de nombreux composants différents (des Factory Methods pour la création d'objets, un gestionnaire de Prototypes, un gestionnaire pour la mise en cache de certains objets Proxy…). On a alors besoin de simplifier l'interface permettant d'accéder à ces objets. Pour cela, on utilise une Facade.

La Facade consiste à créer une interface unique pour accéder à un sous-système. Elle permet donc de créer donc le point d'entrée pour un package. La Facade est unidirectionnelle : de l'extérieur (les modules utilisant le package) vers l'intérieur (le package lui-même) :

Le pattern Facade

Lorsqu'il y a des échanges réciproques entre les composants du système, on parle plutôt de Mediator :

Le pattern Mediator

Le pattern Mediator permet de définir les interactions entre composants d'un même système.

La Facade étant unique, elle peut-être implémentée comme un Singleton, ou alors n'être composée que de méthodes statiques.

VI - Mise en œuvre

Cette section permet de donner une vue d'ensemble des différents patterns abordés dans cet article afin de se concentrer plus spécifiquement sur leur interaction et leur mise en œuvre.

VI-A - Séparation Instanciation/Données/Traitements

En Programmation Orientée-Objets « classique », on apprend que dans une classe, on a :

1. Des constructeurs pour créer les instances de cette classe (ne pas oublier le destructeur, pour libérer la mémoire) ;

2. Des attributs, les données que contiennent les objets ;

3. Des méthodes, qui permettent d'accéder et de modifier ces données.

Changer l'un de ces éléments signifie modifier la liste des constructeurs, des attributs ou des méthodes, avec des répercussions sur l'ensemble de la hiérarchie des classes.

Avec les Design Patterns, il est possible de séparer très clairement ces 3 phases en 3 hiérarchies distinctes :

1. L'instanciation peut se faire à l'aide d'une Factory Method ou d'une Abstract Factory, par exemple ;

2. La hiérarchie des objets sert uniquement de support de données (attributs). Cette hiérarchie peut être constituée d'objets Composite et/ou Decorator ;

3. Les traitements sur les données peuvent être faits au moyen de Visiteurs.

Séparation Création/Objets/Traitements

Les Design Patterns, permettent souvent ce genre de séparations. Ainsi le pattern MVC (Model-View-Controler, qui n'est pas un pattern du GoF, mais est issue du langage Smalltalk) permet de séparer le modèle (les données) de leurs représentation et interaction. Le pattern MVC est notamment repris dans les applications Java EE pour séparer les données et traitements (Model) de l'affichage de ces données (View). Le Controler sert à analyser les requêtes du client, à appeler le modèle correspondant avec la vue adéquate.

Pourquoi des telles séparations ? Pour rendre les modules plus indépendants (ex-: séparer les données métier de leur représentation en base) et ainsi faciliter leur développement. La séparation en modules facilite l'organisation en équipes : plusieurs équipes peuvent travailler sans interférer sur le travail des autres.

VI-B - Aspect modulaire des Design Patterns

Il est intéressant de remarquer que si les Patterns permettent le développement d'applications modulaires (par l'utilisation d'objets composites et/ou interchangeables), les Design Patterns eux-mêmes présentent un caractère modulaire.

En effet, pour un problème donné, les Design Patterns proposent souvent plusieurs solutions distinctes en présentant les avantages, les limitations, les contextes de mise en œuvre, etc.

Ainsi, pour différentes parties de l'application on a le choix entre plusieurs Patterns (interchangeables et parfois même combinables !) :

- Représentation des données : Decorator / Composite / Strategy ;

- Création des objets : Factory Method / Prototype / Builder ;

- Ajout de traitements : Visitor ;

- Gestion des objets distants : Proxy ;

- Coordination des composants : Facade / Mediator.

Dans cet article, nous avons aussi vu la démarche utilisée avec les Design Patterns : pour un problème donné (ex. : mettre en œuvre des objets modulables), on a souvent plusieurs solutions possibles proposées par les Design Patterns. Chaque solution soulève éventuellement de nouvelles questions auxquelles on n'aurait pas forcément pensé de prime abord (ex. : la création et la gestion de ces objets), la réponse à ces questions pouvant se trouver dans d'autres Patterns.

Ainsi, de proche en proche, on construit une solution cohérente pour un problème donné, par assemblage de solutions existantes, reconnues et éprouvées.

C'est pour cela que l'on dit des Design Patterns qu'ils facilitent la transmission de savoir entre informaticiens expérimentés et débutants : les solutions exposées dans les Design Patterns sont connues du plus grand nombre, facilitent la communication, et tendent vers des solutions relativement standard et réutilisables dans différents contextes. Ce qui demande juste un peu de pratique, c'est d'apprendre à assembler ces différentes solutions afin d'obtenir un tout cohérent.

Conclusion

Mine de rien, le contenu de cet article est assez dense pour le débutant (mais aussi pour les autres) :

- on aborde en introduction les limites de la programmation par simple héritage, surcharge et polymorphisme ;

- on présente les notions de Patterns et de modules ;

- on passe rapidement en revue quelques-uns des Design Patterns les plus utilisés ;

- on découvre le processus qui permet, par raisonnements successifs et de proche en proche, d'élaborer une solution quasi complète par assemblage de patterns ;

- on comprend un peu mieux ce que sont les Design Patterns, à quoi ils servent, comment les mettre en œuvre ;

- on comprend mieux la notion de modules, l'interchangeabilité, l'intérêt de la séparation entre données, traitements et instanciation.

Nous avons une définition plus précise (bien qu'encore incomplète) de ce qu'est un module :

- des modules peuvent être des éléments (le plus souvent interchangeables) capables de se combiner entre eux pour créer de nouveaux objets ou de nouveaux comportements (ex.: Composite, Decorator, Strategy) ;

- une organisation en modules permet de rendre les objets plus indépendants les uns des autres, ce qui facilite l'extension de l'application ainsi que le partage des tâches entre programmeurs (ex-: séparation Instanciation/Données/Traitements, séparation Model/View/Controler).

En résumé, deux aspects se dégagent de cet article :

1. L'aspect purement technique portant sur la mise en œuvre des Design Patterns (quels patterns et pour quoi faire ?) ;

2. L'aspect plus général sur la gestion de projets et l'organisation d'une application en modules indépendants.

À la lecture de cet article, vous devriez avoir une meilleure connaissance de ce que sont les Design Patterns, quel est leur intérêt, comment les mettre en œuvre. Toutefois il est nécessaire de garder à l'esprit que nous n'avons fait qu'un survol de certains patterns importants afin d'en exposer le principe et l'intérêt. Cette description ne peut en aucun cas se suffire à elle-même (il manque notamment des détails concernant l'implémentation, la discussion sur l'applicabilité et les limites de chaque pattern, etc.) et il est absolument indispensable de se documenter plus sérieusement sur chacun des patterns exposés (notamment ceux qui vous intéressent).

Pour cela, plongez-vous dans la lecture de Design Patterns: Elements of Reusable Object-Oriented Software par Gamma et al., qui est la référence dans le domaine. Quelques ressources sont également disponibles sur le site de developpez (voir liens externes).

Une fois ces connaissances acquises et assimilées, et après un peu de pratique sur l'utilisation des patterns (identification des problèmes, recherche des patterns adaptés, mise en œuvre de la solution), on comprend assez vite l'intérêt de telles solutions, qui peuvent paraître assez lourdes pour de petits programmes, mais qui deviennent quasiment incontournables dans des projets d'envergure réunissant plusieurs personnes.

Liens externes

Pour ceux qui souhaitent approfondir leur connaissance des Design Patterns, j'ai aussi écrit un article spécifiquement sur le pattern Etat.

Le livre Design Patterns (Gamma et al.), le livre de référence en version française.

Remerciements

Merci à Miles, wichtounet, Nip et jerome.petit pour la relecture de cet article.

Téléchargement

Cet article au format PDF