I. Définition de design pattern▲
Le premier article sur le Design pattern singletonWinDev et le Design pattern : singleton donne une définition des design patterns (ou encore « patron de conception » en français), il en existe trois types :
- construction : ils définissent comment faire l'instanciation et la configuration des classes et des objets ;
- structure : ils définissent comment organiser les classes d'un programme dans une structure plus large (séparant l'interface de l'implémentation) ;
- comportement : ils définissent comment organiser les objets pour que ceux-ci collaborent (distribution des responsabilités) et expliquent le fonctionnement des algorithmes impliqués.
Dans cet article, nous allons voir la mise en place du design pattern Stratégie de type comportemental.
II. Le design pattern Stratégie▲
Le design pattern Stratégie définit un objet représentant un algorithme dédié à une tâche spécifique.
II-A. Description du problème▲
En programmation objet, une classe est composée :
- d'attributs représentant l'état des objets ;
- de méthodes représentant leur comportement.
Lors de l'identification d'une nouvelle tâche, le premier réflexe est de créer une classe représentant et gérant celle-ci. À l'occasion de demandes clients et des évolutions techniques, cette tâche est amenée à évoluer, on développe une notion d'héritage en complexifiant le modèle d'origine (ajout de méthodes et de membres). Les méthodes (comportements) peuvent être communes à plusieurs classes, sous-classes et complexifient encore plus le diagramme de classes. Dans notre problématique, nous devons prendre en compte que toute modification de l'existant peut engendrer des modifications de comportement et donc des régressions.
II-B. La solution▲
Avant de donner la solution clé en main, nous allons lister les critères que la solution doit respecter :
- éviter la duplication de code ;
- encapsuler les différences ;
- faciliter les évolutions ;
- éviter l'effet de bord.
Et particulièrement pour ce design pattern : - utiliser la composition plus que l'héritage ;
- créer des superclasses de type comportemental ;
- permettre une évolution du comportement en cours de programme.
III. Programmation du design pattern▲
Après la théorie, passons à la pratique avec la mise en place du design pattern Stratégie au travers d'un exemple adapté du livre « Design Pattern - la tête la première », il permettra de voir l'évolution progressive d'un programme et les problèmes rencontrés et l'intérêt du design pattern Stratégie.
III-A. Le jeu super canard▲
III-A-1. La première version▲
Le programme de base est un jeu de canards évoluant dans une mare, se déplaçant et émettant des bruits. Dans la première version, plusieurs types de canards existent :
- les colverts ;
- les mandarins.
Les développeurs ont conçu le programme en orienté objet, avec une super classe ac_canard .
ac_canard est
une
Classe
//Membres
FIN
PROCEDURE
PROTÉGÉE Constructeur()
PROCEDURE
Destructeur
()
PROCEDURE
cancaner()
RENVOYER
"Je suis un canard et je cancane"
PROCEDURE
nager()
RENVOYER
"Je suis un canard et je nage"
PROCEDURE
PROTÉGÉE afficher()
RENVOYER
""
La classe de base gère les actions communes aux canards (nager et cancaner).
La procédure afficher() est protégée, car celle-ci doit être redéfinie dans toutes les classes filles.
Et deux classes pc_colvert et pc_mandarin représentant les canards.
pc_colvert est
une
Classe
hérite
de
ac_canard
FIN
PROCEDURE
Constructeur()
PROCEDURE
Destructeur
()
PROCEDURE
VIRTUELLE afficher()
RENVOYER
"Afficher un colvert"
pc_mandarin est
une
Classe
hérite
de
ac_canard
FIN
PROCEDURE
Constructeur()
PROCEDURE
Destructeur
()
// Résumé : <indiquez ici ce que fait la procédure>
PROCEDURE
VIRTUELLE afficher()
RENVOYER
"Affiche un mandarin"
Suivant le même principe on peut mettre en place d'autres canards.
Les classes filles implémentent leur méthode afficher(). Effectivement, ce sont tous des canards mais ils ont un aspect visuel différent.
La première étape de conception est réalisée et la première version du jeu est finalisée.
III-A-2. Les évolutions…▲
Dans la deuxième version, une évolution doit être proposée : la prise en compte de canards volants. Comme le développement a été effectué en objet, cela ne devrait pas poser de problèmes.
Dans le code on ajoute une fonction voler() dans la classe ac_canard.
PROCEDURE
voler()
RENVOYER
« Je suis un
canard volant »
Avec l'implémentation de la fonction voler() dans la super classe, l'ensemble des sous-classes héritent de cette évolution et tous les canards volent. Les premiers tests révèlent un problème : tous les canards volent. Lors de la création de la fonction, on a oublié de prendre en compte que certains canards comme les canards en plastique ne volent pas.
Par exemple la classe pc_canardEnPlastique :
pc_canardEnPlastique est
une
Classe
hérite
de
ac_canard
FIN
PROCEDURE
Constructeur()
PROCEDURE
Destructeur
()
PROCEDURE
VIRTUELLE cancaner()
RENVOYER
"Je suis un canard et je couine"
Dans cette classe, on a déjà redéfini la méthode cancaner(), on peut donc redéfinir la méthode voler().
PROCEDURE
VIRTUELLE voler()
RENVOYER
"Je suis un canard qui ne vole pas"
Toujours suivant le même principe, on peut avoir des canards en bois représentés par la classe suivante :
pc_canardEnBois est
une
Classe
hérite
de
ac_canard
FIN
PROCEDURE
Constructeur()
PROCEDURE
Destructeur
()
PROCEDURE
VIRTUELLE voler()
RENVOYER
"Je suis un canard qui ne vole pas"
III-A-3. Héritage multiple▲
Dans la vie de notre jeu, on sait que les spécifications vont changer, il nous faudra redéfinir les méthodes voler() et cancaner()pour toutes les nouvelles sous-classes ajoutées au programme avec un comportement contraint par la super classe. Une nouvelle conception plus judicieuse et plus compréhensible doit être adoptée pour permettre à certains canards (pas tous) de voler et de cancaner.
Une solution est d'extraire les méthodes cancaner et voler des superclasses ac_volant et ac_cancaneur avec respectivement, les méthodes voler() et cancaner(). Avec l'héritage multiple, les sous-classes hériteront et redéfiniront les comportements souhaités.
Dans les langages tels que Java et .NET, l'héritage multiple est remplacé par des interfaces. L'utilisation d'héritage multiple est une solution de contournement.
Développons cet aspect :
ac_volant est
une
Classe
FIN
PROCEDURE
Constructeur()
PROCEDURE
Destructeur
()
PROCEDURE
voler()
//Méthode à redéfinir dans les sous-classes
ac_cancaneur est
une
Classe
FIN
PROCEDURE
Constructeur()
PROCEDURE
Destructeur
()
PROCEDURE
cancaner()
//Méthode à redéfinir dans les sous-classes
On modifie la classe ac_canard pour ne conserver que la méthode afficher() et nager() et on la met en place dans les sous-classes.
Codes des sous-classes mises à jour pc_colvert et pc_mandarin :
pc_colvert est
une
Classe
hérite
de
ac_canard
hérite
de
ac_volant
hérite
de
ac_cancaneur
FIN
PROCEDURE
Constructeur()
PROCEDURE
Destructeur
()
PROCEDURE
VIRTUELLE afficher()
RENVOYER
"Afficher un colvert"
PROCEDURE
VIRTUELLE voler()
RENVOYER
"Je suis un canard et je vole"
PROCEDURE
VIRTUELLE cancaner()
RENVOYER
"Je suis un canard et je cancane"
pc_mandarin est
une
Classe
hérite
de
ac_canard
hérite
de
ac_volant
hérite
de
ac_cancaneur
FIN
PROCEDURE
Constructeur()
PROCEDURE
Destructeur
()
PROCEDURE
VIRTUELLE afficher()
RENVOYER
"Affiche un mandarin"
PROCEDURE
VIRTUELLE cancaner()
RENVOYER
"Je suis un canard et je cancane"
PROCEDURE
VIRTUELLE voler()
RENVOYER
"Je suis un canard et je vole"
Après la mise à jour des deux sous-classes, on se rend compte que cette conception duplique du code et ne respecte pas un critère, avec pour conséquences :
- d'empêcher la réutilisabilité ;
- de compliquer la maintenance.
Dans cette solution nous avons utilisé des classes abstraites comme des interfaces, c'est-à-dire sans code. Le code des méthodes pourrait être implémenté dans les fonctions voler() et cancaner(), mais cette solution ne serait pas pérenne en cas d'évolution nécessitant deux possibilités différentes de voler. Il faudrait alors repasser sur toutes les sous-classes pour prendre en compte ou pas cette nouvelle possibilité…
Pour faciliter l'évolution, la conception d'un logiciel doit prendre en charge le changement.
III-A-4. La solution▲
Nous avons vu dans les paragraphes précédents :
- l'héritage : avec un diagramme de classes se complexifiant rapidement ;
- l'héritage multiple : solution prometteuse avec l'extraction des comportements, mais un nouveau comportement, demande une recherche dans toutes les sous-classes et une modification de certaines.
Dans toute idée il n'y a pas que du mauvais, l'héritage multiple est un début de réflexion sur la séparation des comportements.
La solution est de séparer les aspects qui varient de ceux qui sont constants, une définition plus informaticienne : extraire les parties variables et les encapsuler pour permettre de les modifier ou de les augmenter sans affecter celles qui ne varient pas. Mettons en place cette solution dans l'exemple de nos canards. Ce qui nous pose problème depuis le départ ce sont les procédures voler()et cancaner(). Nous allons créer deux ensembles de classes, chacun contiendra les implémentations des classes des comportements respectifs et les procédures voler()et cancaner().
Pour le comportement de cancaner nous aurons les classes suivantes :
- pc_cancaner ;
- pc_couiner ;
- pc_silence.
Pour le comportement voler nous aurons les classes suivantes :
- pc_volerAvecAiles;
- pc_nePasVoler.
Nous allons représenter les comportements cancaner et voler en deux classes abstraites :
- ac_comportementVoler ;
- ac_comportementCancaner.
et chaque classe comportementale héritera d'une de ces classes.
Dans un langage tel que JAVA et .Net, il est préférable d'utiliser les interfaces au lieu des classes abstraites. L'idée est de mettre en place une conception pour utiliser le principe de polymorphisme.
À la différence avec nos essais précédents, les comportements provenaient de la classe ac_canard où étaient redéfinis dans les sous-classes. Avec cette conception, les sous-classes de canard auront un comportement représenté par les classes abstraites ac_cancaner et ac_voler, tel que le comportement ne soit pas dans les sous-classes.
Codes des superclasses comportementales :
ac_comportementCancaner est une Classe
FIN
PROCEDURE Constructeur()
PROCEDURE Destructeur()
PROCEDURE cancaner()
RENVOYER ""
//A REDEFINIR DANS LES SOUS-CLASSES COMPORTEMENTALES
ac_comportementVoler est
une
Classe
FIN
PROCEDURE
Constructeur()
PROCEDURE
Destructeur
()
PROCEDURE
voler()
RENVOYER
""
//A REDEFINIR DANS LES SOUS-CLASSES COMPORTEMENTALES
Les codes des sous-classes du comportement voler doivent implémenter uniquement voler() :
pc_volerAvecAiles est
une
Classe
hérite
de
ac_comportementVoler
FIN
PROCEDURE
Constructeur()
PROCEDURE
Destructeur
()
PROCEDURE
VIRTUELLE voler()
RENVOYER
"Je suis un canard qui vole avec des ailes"
pc_nePasVoler est
une
Classe
hérite
de
ac_comportementVoler
FIN
PROCEDURE
Constructeur()
PROCEDURE
Destructeur
()
PROCEDURE
VIRTUELLE voler()
RENVOYER
"je suis un canard qui ne vole pas"
Les codes des sous-classes du comportement cancaner doivent implémenter uniquement cancaner() :
pc_cancaner est
une
Classe
hérite
de
ac_comportementCancaner
FIN
PROCEDURE
Constructeur()
PROCEDURE
Destructeur
()
PROCEDURE
VIRTUELLE cancaner()
RENVOYER
"Je suis un canard et je cancane"
pc_couiner est
une
Classe
hérite
de
ac_comportementCancaner
FIN
PROCEDURE
Constructeur()
PROCEDURE
Destructeur
()
PROCEDURE
VIRTUELLE Cancaner()
RENVOYER
"Je suis un canard qui couine"
pc_couiner est
une
Classe
hérite
de
ac_comportementCancaner
FIN
PROCEDURE
Constructeur()
PROCEDURE
Destructeur
()
PROCEDURE
VIRTUELLE Cancaner()
RENVOYER
"Je suis un canard qui couine"
Avec cette conception, on peut réutiliser les comportements pour d'autres objets et on peut créer de nouveaux comportements sans toucher aux existants et aux classes canards. L'autre intérêt de cette conception, la classe ac_canard va déléguer les comportements. Maintenant que nous avons programmé nos comportements, nous allons créer la liaison entre la classe ac_canard et les comportements avec les opérations suivantes :
- création de deux nouveaux membres de type superclasses comportementales ;
- création de deux méthodes cancaner() et voler() faisant appel aux méthodes des superclasses et avec le polymorphisme les méthodes des classes comportementales.
ac_canard est
une
Classe
lo_comportementCancaner est
un
ac_comportementCancaner dynamique
lo_comportementVoler est
un
ac_comportementVoler dynamique
FIN
PROCEDURE
PROTÉGÉE Constructeur()
PROCEDURE
Destructeur
()
PROCEDURE
PROTÉGÉE afficher()
RENVOYER
""
PROCEDURE
VIRTUELLE voler()
RENVOYER
:
lo_comportementVoler:
voler()
PROCEDURE
VIRTUELLE cancaner()
RENVOYER
:
lo_comportementCancaner:
cancaner()
Dans les méthodes cancaner()et voler(),nous observons la délégation des comportements de la classe ac_canard. La dernière chose à mettre en place est l'affectation des comportements aux canards.
Dans le constructeur des sous-classes canard on alloue les comportements :
pc_colvert est
une
Classe
hérite
de
ac_canard
FIN
PROCEDURE
Constructeur()
:
lo_comportementCancaner=
allouer
un
pc_cancaner()
:
lo_comportementVoler=
allouer
un
pc_volerAvecAiles()
PROCEDURE
Destructeur
()
PROCEDURE
VIRTUELLE afficher()
RENVOYER
"Afficher un colvert"
Dans le même principe, vous pouvez coder les autres sous-classes deac_canard.
Maintenant, testons notre conception et notre programmation :
lo_canard est
pc_colvert()
Trace
(
lo_canard:
afficher())
Trace
(
lo_canard:
cancaner())
Trace
(
lo_canard:
voler())
Afficher un
colvert
Je suis un
canard et
je cancane
Je suis un
canard qui vole avec
des
ailes
Si vous ne constatez pas le même résultat, vérifiez que vous avez rajouté le mot-clé « VIRTUELLE » dans la définition des procédures surchargées.
Faisons le point sur les critères devant respecter la solution :
- éviter la duplication de code ;
- encapsuler les différences ;
- faciliter les évolutions ;
- éviter l'effet de bord ;
- utiliser la composition plus que l'héritage ;
- créer des superclasses de type comportemental ;
- permettre une évolution du comportement en cours de programme.
Nous avons respecté tous les critères sauf le dernier, l'évolution du comportement. Effectivement, on alloue un comportement dans le constructeur des sous-classes de ac_canard, mais on n'a pas prévu l'évolution du changement de comportement en cours de programme. Afin de mettre en place ce critère, nous allons créer deux méthodes dans la classe ac_canard :
- setComportementVoler () ;
- setComportementCancaner ().
Ces méthodes permettent de changer dynamiquement le comportement des canards.
Code de ces méthodes :
PROCEDURE
setComportementVoler(
po_comportementVoler est
un
ac_comportementVoler dynamique
)
:
lo_comportementVoler=
po_comportementVoler
PROCEDURE
setComportementCancaner(
po_ComportementCancaner est
un
ac_comportementCancaner dynamique
)
:
lo_comportementCancaner=
po_ComportementCancaner
Dans ces fonctions, on passe en paramètre le nouveau comportement et on l'affecte au membre.
Reprenons notre précédent test et ajoutons une modification de comportement.
lo_canard est
pc_colvert()
Trace
(
lo_canard:
afficher())
Trace
(
lo_canard:
cancaner())
Trace
(
lo_canard:
voler())
lo_canard:
setComportementVoler(
allouer
un
pc_nePasVoler())
Trace
(
"---- Changement de comportement"
)
Trace
(
lo_canard:
voler())
Afficher un
colvert
Je suis un
canard et
je cancane
Je suis un
canard qui vole avec
des
ailes
----
Changement de
comportement
je suis un
canard qui ne vole pas
IV. Remerciements▲
- relecture technique : LittleWhite ;
- relecture orthographique : f-leb ;