I. Introduction▲
La notion de Design Patterns a été cristallisée par le livre Design Patterns, Element of Reusable Object Oriented Software d'Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides (paru chez Addison Wesley en 1995).
Il ne s'agit pas d'un nouveau concept à la mode pour illuminés de la programmation.
Mais d'une réflexion de programmeurs sur leurs pratiques.
« Chaque pattern décrit un problème qui se présente souvent dans notre environnement, et décrit le cœur d'une solution à ce problème sur un mode qui permet d'utiliser cette solution des millions de fois, sans jamais avoir à refaire deux fois la même chose ». (Christophe Alexander cité par Gamma, et al.)
À force de développer des classes et de les faire évoluer dans le temps, il apparaît au programmeur que certains dessins de relations entre classes se répètent. L'idée se forme alors, au fil de l'expérience, de regrouper des situations ayant été résolues par des dessins identiques de liaisons entre classes.
C'est de cette réflexion sur l'expérience que sont nés les Design Patterns.
Leur but est donc :
- de faciliter l'identification des problèmes d'architecture logicielle ;
- de les rapporter au catalogue des solutions connues.
Pour les différencier des « trucs et astuces », on soulignera que les solutions des Design Patterns s'expriment avec un souci d'abstraction qui permet une utilisation concrète plus large de la solution.
II. Mediator▲
Commençons par un Design Pattern que nous utilisons tous les jours, peut-être sans le savoir.
Prenons par exemple les gestionnaires d'événements d'une forme : le clic sur un bouton déclenche
procedure
Form0.Button1OnClick(Sender : TObject);
begin
ShowMessage('T''as cliqué'
);
end
;
L'événement OnClick est attaché à l'objet Button1, qui au sens Windows est une fenêtre.
Comment se fait-il donc que la déclaration de l'événement soit dans TForm1 et non dans TButton1 ?
TForm1 = class
(TForm)
Button1 : TButton;
procedure
Button1OnClick(Sender : TObject);
...
end
;
et non
TButton1 = class
(TButton)
procedure
Click;
...
end
;
Button1 ne va pas traiter directement le clic, mais envoyer à la forme parente le message BN_CLICKED.
La première écriture que nous utilisons dans Delphi suppose donc une redirection de l'événement du bouton Button1 vers le gestionnaire d'événement de la forme Form1.Button1OnClick.
C'est le pattern Mediator qui est utilisé ici.
Il apporte une économie d'utilisation de Delphi puisque les gestionnaires d'événements remontés au niveau de la forme peuvent plus facilement interagir entre eux.
La TForm est le Mediator, et le bouton, le Mediated.
Écrire le gestionnaire d'événement au sein de la TForm est devenu tellement naturel que l'on a oublié ce que l'on aurait dû écrire sans ce mécanisme : une classe descendant de TButton, dans laquelle on aurait surchargé la méthode Click. Notre code s'en serait trouvé alourdi, d'autant que nous aurions eu à déclarer une classe descendante pour chaque composant de la forme.
Inscrire le gestionnaire dans TForm1 simplifie également l'apprentissage de Delphi : le bouton est posé dans la forme, le gestionnaire d'événement aussi. Cela parait plus logique pour celui qui débute, même si, en réalité, les choses se passent différemment : c'est le gestionnaire de TButton1 que Delphi construit en arrière-plan qui reçoit le clic et appelle à son tour Button1OnClick.
Le pattern Mediator se représente de la manière suivante :
Nous avons là un pattern, car
- nous répondons à un problème récurrent : réduire le nombre de classes à manipuler et écrire plus simplement les interactions entre événements ;
- par une solution qui peut se dessiner (design) sous une forme (pattern) assez générale pour en permettre la réutilisation.
Dans le cas qui nous occupe, le Mediator est la Forme Form1 : elle rassemble les événements des composants.
Le Mediated est le bouton Button1, (ainsi que tous les composants de la forme).
Et l'événement, liaison de médiation, est le clic.
Nota : la fonction Wire sert à suspendre la redirection d'événement de manière contrôlée par le programme.
Ce paragraphe nous a fait toucher du doigt un exemple de pattern.
Nous allons maintenant regarder quelques patterns très importants et voir comment ModelMaker nous aide à les mettre en pratique.
III. Wrapper▲
En Delphi, le polymorphisme découle de l'héritage : deux classes vont pouvoir être appelées dans la même méthode d'un ancêtre commun. Cela suppose donc qu'elles descendent de cet ancêtre. Par exemple (tiré de la documentation de ModelMaker), vous avez une classe TExemple qui descend de TObject. Vous souhaitez l'ajouter à la palette de composants de Delphi. Or les composants doivent descendre de TComponent. Vous pourriez, évidemment faire descendre TExemple de TComponent plutôt que de TObject… si vous aviez les sources. Mais supposons que vous ne les avez pas. Comment faire ? Il faut que TExemple soit vu comme un TComponent.
La solution qui le permet consiste en l'application du pattern Wrapper.
Par les relations uses que ce pattern met en œuvre, il fournit une manière d'approcher dans Delphi l'héritage multiple : la classe TExemple, une fois enveloppée dans le Wrapper, contiendra les méthodes et propriétés publiques de TExemple et les méthodes et propriétés publiques (et protégées) de TComponent.
Voyons directement comment nous faisons avec ModelMaker.
Nota : pour l'utilisation de ModelMaker, veuillez vous reporter à l'article ModelMaker de Delphi 7
Nous ouvrons un projet dans ModelMaker, et nous affichons un nouveau diagramme de classes.
(F5 et clic sur l'icône Add Diagram Class, la première des 12 icônes de diagramme à gauche).
Nous ajoutons deux classes en cliquant sur l'icône Add Class to Model
La première classe est nommée TExemple et mise en Placeholder (case à cocher au milieu à droite du panneau Class Symbol), car nous n'avons pas son code.
Avant de placer la deuxième classe qui devra descendre de TComponent, nous ajoutons TComponent en tant que Placeholder, (en procédant comme pour TExemple).
Puis la deuxième classe est ajoutée. Nous la nommons TWrapperExemple : nous indiquons dans le panneau Class Symbol qu'elle descend de TComponent. Cette classe n'est pas en Placeholder : nous allons créer son code.
À cette étape, notre diagramme doit comporter trois rectangles. Pour faire apparaître les relations, nous faisons Ctrl+A pour sélectionner les trois classes. Nous cliquons droit et choisissons Wizards dans le menu contextuel, puis Visualize Class Relations.
Le diagramme doit avoir alors l'allure suivante :
Imaginons que la documentation de TExemple (dont nous n'avons pas le code) nous indique cependant qu'elle contient la propriété Valeur et la méthode Calculer. Nous allons pouvoir ajouter ces éléments à notre modèle : nous cliquons sur TExemple dans le diagramme. Elle doit avoir le focus dans la hiérarchie des classes en haut à droite :
Nous ajoutons la propriété Valeur en cliquant sur Write Access est mis à Field et nous cliquons sur OK.
Nous ajoutons la méthode Calculer en cliquant sur Nous cliquons sur OK.
Nota : à ce niveau, il peut être sage de sauvegarder le modèle en cliquant sur l'icône disquette.
Maintenant, nous allons ajouter à TWrapperExemple une propriété TExemple : en effet, c'est par une relation de ce type, appelée uses que TWrapperExemple pourra atteindre les méthodes et propriétés de TExemple.
Dans le diagramme de classe à droite, nous cliquons sur , puis nous effectuons un glisser-déplacer du rectangle WrapperExemple vers le rectangle TExemple. La propriété est automatiquement créée.
ModelMaker nous a servi jusqu'à présent d'outil de dessin. Mais il va nous rendre grand service, car nous allons respecter une des règles de la programmation-objet : un objet ne doit pas appeler les sous-méthodes d'un autre objet. Cette règle, appelée règle de Demeter signifie qu'un utilisateur de notre WrapperExemple qui veut provoquer un calcul ne devra pas écrire WrapperExemple1.Exemple.Calculer, mais WrapperExemple1.Calculer. Pourquoi ? Eh bien parce que si l'on fait remonter à plus de 1 niveau les propriétés et méthodes, on se trouve rapidement devant une structure complexe : toute modification dans une méthode (par exemple changer le nom de Calculer) devra se propager à tous les objets utilisant un TWrapperExemple. Ceci va à contre-courant de la règle d'encapsulation de la programmation-objet. En revanche, si nous créons dans TWrapperExemple une méthode Calculer qui fera appel à Exemple.Calculer nous contenons les effets de propagation d'un changement de nom à la seule classe TWrapperExemple. Les objets qui utilisent TWrapperExemple pourront continuer d'appeler Calculer sans se soucier des modifications internes.
C'est par le respect de cette discipline d'écriture que l'on obtient des programmes faciles à lire et à maintenir. Elle est à mettre en œuvre dès le début du projet, justement là où, tout étant simple, on serait tenté de ne pas l'appliquer.
ModelMaker va nous aider à construire automatiquement toutes les méthodes d'encapsulation.
Cliquez sur l'icône Patterns située tout en haut.
Sélectionnez dans la hiérarchie de gauche la classe TWrapperExemple.
Et en bas, le champ Exemple. Choisissez dans l'onglet Structural la pattern Wrapper (le plus à gauche).
Un panneau s'ouvre. Passez tout à droite et cliquez sur OK : toutes les méthodes sont créées.
Nota : sur cet exemple simplifié à l'extrême pour la clarté de l'exposé, vous pouvez penser que vous auriez été plus vite à la main. Mais sur des exemples réels, le gain est très important : vous pouvez réaliser en un clic le travail qui vous aurait pris des heures.
Votre diagramme de classe devrait vous apparaître comme ceci :
Pour voir les propriétés dans le diagramme, cliquez droit dedans. Dans le menu contextuel, choisissez Diagram properties. Décochez Project member type filter et cochez les quatre cases sous Custom member type filter et OK.
Voyons maintenant comment ModelMaker gère la relation de pattern que nous avons mise en place.
Cliquez en bas à droite sur la méthode Calculer de TWrapperExemple. Puis regardez l'implémentation (F6). Essayez d'en modifier le code Exemple.Calculer;. Vous ne pouvez pas le faire. En effet, ModelMaker considère que le « propriétaire » de cette méthode est le pattern que vous avez mis en place. Maintenant, allez sur la méthode Calculer de TExemple et changez son nom, par exemple en CalculerTick. Ce changement nom est répercuté à tous les niveaux, y compris dans le nom de la méthode Calculer de TWrapperExemple qui devient CalculerTick.
Mais nous avions dit plus haut qu'il ne devait pas y avoir de propagation !
Ici, non seulement il y a propagation, mais de plus, si vous allez à nouveau dans la liste des Patterns et que vous supprimez par le menu contextuel (clic droit) le pattern que vous venez de mettre en place toutes les méthodes de « liaisons » vont être supprimées : le couplage est donc total.
Réfléchissons deux minutes : lors de l'établissement d'un modèle, il y a une première phase rapide, dans laquelle on pose les noms des classes, des méthodes et des propriétés, et dans laquelle on ajoute des liens de type Pattern. Il peut y avoir des fautes de frappe, des noms à changer, des tests fait sur plusieurs architectures possibles : quelle facilité de pouvoir corriger, modifier ou supprimer un élément et de voir le modèle entier conserver sa cohérence.
Une fois cette phase terminée, et pour disposer de l'indépendance souhaitable, nous procéderons au découplage du pattern. Pour ce faire, vous cliquez droit sur le pattern dans la liste des patterns (Icône ) et vous choisissez Release ownership. Si vous revenez maintenant sur TExemple pour changer le nom de Calculer, il n'y a plus de propagation. Vous devrez même aller changer à la main Exemple.Calculer;, mais dans la seule classe TWrapperExemple.
(Faites le découplage au bon moment, car il est irréversible).
Vous pouvez télécharger le modèle ici
En résumé, le pattern Wrapper se présente ainsi :
Il permet qu'un objet soit vu comme le descendant d'une autre hiérarchie : ici TWrapped sera vu dans l'application au travers de TWrapper avec l'ascendance de TWrapper.
Après avoir traité complètement un pattern dans ModelMaker, nous allons passer en revue d'autres patterns proposés par ModelMaker.
IV. Visitor▲
Ce pattern permet d'ajouter des méthodes à une hiérarchie de classes, mais en les plaçant dans une autre hiérarchie.
Prenons l'exemple d'une gestion de prêts bancaires :
Ces prêts peuvent être calculés selon plusieurs méthodes financières. Au lieu de mettre ces méthodes dans la hiérarchie des prêts, nous les mettons dans une hiérarchie propre :
Ceci présente plusieurs avantages.
La gestion du prêt est rassemblée dans son objet propre, tandis que les outils de calcul le sont dans un autre. La maintenance du code est plus aisée et peut même être confiée à des personnes différentes.
De plus, si on ajoute une nouvelle méthode de calcul de prêt, par exemple les prêts à taux variables, celle-ci peut être facilement ajoutée à sa propre hiérarchie TCalculateur sans avoir à modifier la classe des prêts : il suffit d'y mettre une procédure générique qui parcourt les enfants de TCalculateur pour avoir les résultats du coût d'un prêt selon les différentes méthodes de calcul.
Le schéma général du pattern Visitor se présente ainsi :
V. Decorator▲
Ce pattern va permettre d'ajouter des propriétés et des méthodes à une classe en les plaçant « à côté » plutôt qu'à l'« intérieur » ou par héritage. Cela permet une articulation plus facilement modulable et localisable des parties de la classe. Le pattern Decorator enveloppe le tout (le décore) pour que cela apparaisse au reste de l'application comme une classe unique :
VI. Observer▲
Ce pattern implémente un système de sémaphore qui prévient les objets qui sont reliés à un objet maître d'un changement d'état de ce dernier :
VII. Autres patterns▲
Lock
Ce pattern permet de mettre en lecture seule une partie des propriétés d'une classe.
Il est utilisé dans Delphi pour mettre un TStrings en lecture seule.
Singleton
Ce pattern est utile quand une classe ne doit avoir qu'une seule instance : par exemple si cette classe est celle d'un objet qui doit être unique.
Reference count
Ce pattern implémente un compteur du nombre d'instances d'un objet. Lorsque l'objet n'est plus référencé, il s'autodétruit.
VIII. Conclusion▲
Nous avons passé en revue les principaux patterns proposés par ModelMaker.
Le fait que ModelMaker institue le pattern comme propriétaire du code du pattern (pour modifier ces parties du code, ModelMaker vous oblige à intervenir au niveau du pattern : vous ne pouvez le faire directement) permet de bien isoler ce code : c'est une grande aide que d'isoler ainsi le code du pattern de celui du reste de la classe.
Quand vous verrez qu'en outre vous pouvez modifier les patterns proposés et même ajouter les vôtres, vous serez à même de mesurer la puissance que procure une bonne utilisation de ModelMaker livré avec Delphi Entreprise et Delphi Architect.
Pour compléter cet article, consultez :