V. Le décorateur▲
Définition: Les décorateurs sont un ensemble de classes permettant d'étendre dynamiquement le rôle d'une classe de base.
V-1. Pourquoi le décorateur▲
Lorsqu'un produit quelconque est conçu, on ne peut jamais prévoir à l'avance toutes ses utilisations c'est pourquoi il faut laisser la possibilité aux utilisateurs d'étendre le produits avec des extensions. Dans le cas des logiciels, ces extensions prennent la forme de plugin écrits par les utilisateurs en respectant une interface donnée par le développeur. Mais comment faire pour les objets ? Comment peut on étendre le rôle d'un objet donné sans étendre toute la classe ? Les décorateurs ont ce rôle. Nous allons étudier un cas, celui des armes dans un jeu de rôle. Le héros possède une Arme qui peut être une arme de contact comme une épée, une hache, ou une arme de jet comme un arc ou une arbalète. Selon le gameplay, le joueur doit pouvoir améliorer ses armes en les rendant enflammant, en les rendant glacés ou en les bénissant. Du point de vue du code, ces armes modifiés devront être manipulable comme des armes normales. Nous allons donc créer une classe Sort qui héritera de la classe Arme servira de canevas pour tous nos décorateurs.
On arrive donc au diagramme suivant:
V-2. Les bases nécessaires avant de créer le pattern▲
Avant de commencer à nous préoccuper du pattern à proprement parler, nous allons introduire une classe personnage qui servira en tant que cible lors d'une attaque.
class
Personnage
{
public
:
void
recevoirDegat(int
degat){
std::
cout<<
"Le personnage a recu "
<<
degat<<
" de degat"
<<
std::
endl;}
}
;
Nous allons maintenant introduire notre classe Arme qui sera une interface via sa fonction calculer_degat.
class
Arme
{
protected
:
const
int
degatMax;
virtual
int
calculer_degat(Personnage&
p) const
=
0
;
public
:
void
attaquer(Personnage&
p)
{
p.recevoirDegat(calculer_degat(p));
}
Arme(int
degMax=
0
):degatMax(degMax){}
}
;
Le code est assez simple. La classe permet d'attaquer un personnage via la fonction membre attaquer. L'attaque a une valeur déterminée, valeur qui est calculée par la méthode calculer_degat, qui est virtuelle pure car dans l'état actuel, on ne peut pas calculer les dégâts qu'une arme va pouvoir infliger.
Nous allons maintenant mettre en place la hiérarchie d'armes.
class
Arc : public
Arme
{
const
int
distanceMax;
protected
:
virtual
int
calculer_degat(Personnage&
p) const
{
return
21
;
}
public
:
Arc(int
degMax,int
distMax):Arme(degMax),distanceMax(distMax){}
}
;
class
Epee : public
Arme
{
public
:
virtual
int
calculer_degat(Personnage&
p) const
{
return
42
;
}
public
:
Epee(int
degatMax):Arme(degatMax){}
}
;
Le code est très simple, rien qui ne nécessite de grandes explications. Il faut néanmoins souligner que les calculs de dégâts sont totalement fantaisistes: ils ne tiennent pas compte des caractéristiques des armes ni du héro qui utilise l'arme. En l'état actuel des choses, ils sont justes là pour montrer le principe du décorateur.
V-3. Le pattern en lui même▲
Nous allons maintenant construire le pattern en lui même. Comme cela a été dit en introduction, l'arme améliorée doit être utilisable comme une arme quelconque d'où l'héritage de la classe Arme par la classe Sort. Enfin, un sort s'applique à une arme, donc notre classe sort aura un attribut noté arme. On arrive donc au premier jet suivant:
//On définit ici notre classe de decoration, des sorts que l'on va pouvoir jetter sur nos armes
class
Sort: public
Arme
{
protected
:
//l'arme sur laquelle le sort va s'appliquer.
boost::
shared_ptr<
Arme>
marme;
public
:
Sort(Arme*
arme):marme(arme){}
}
;
Le rôle de notre classe Sort est de modifier le calculer des dégâts lors de la phase d'attaque. Mais pour le moment, nous ne pouvons pas calculer les modifications précisément, cette classe reste donc abstraite.
De cette classe, nous allons créer des sors qui vont vraiment modifier le calcul des dégâts. Dans notre exemple, la modification du calcul des dégâts se fait en ajoutant un facteur au dégâts de base ou en les multipliant par un facteur. Nous allons regrouper tout ceci dans une classe template Parchemin qui sera paramètrée par une classe Modificateur
//on définit des
template
<
class
Modificateur>
class
Parchemin: public
Sort
{
protected
:
virtual
int
calculer_degat(Personnage&
p) const
{
return
Modificateur::
calculer(marme->
calculer_degat(p));
}
public
:
Parchemin(Arme *
arme):Sort(arme){}
}
;
Les modificateurs qui serviront à générer des classes de parchemins sont au nombre de deux (Add et Multiply) et sont sont eux aussi paramétrés via un int passé en tant que paramètre template.
template
<
int
val>
struct
Add
{
static
int
calculer (int
n) {
return
n+
val;}
}
;
template
<
int
val>
struct
Multiply
{
static
int
calculer (int
n) {
return
n*
val;}
}
;
Il ne nous reste plus qu'à créer des parchemins et à les appliquer sur des armes.
typedef
Parchemin<
Add<
2
>
>
ParcheminFeu;
typedef
Parchemin<
Add<
4
>
>
ParcheminGlace;
typedef
Parchemin<
Multiply<
2
>
>
ParcheminDivin;
int
main(int
argc, char
const
*
argv[])
{
Personnage p;
ParcheminDivin arme(new
ParcheminGlace(new
Epee(20
)));
ParcheminFeu armebis(new
ParcheminGlace(new
Arc(10
,20
)));
armebis.attaquer(p);
return
0
;
}
Néanmoins, ce code ne marchera pas. En effet, dans la classe Parchemin, on fait appel à la fonction calculer_degat de marme. Or cette fonction est protégée, on ne peut pas l'appeler sur un objet depuis l'extérieur de classe.
Une solution serait de la rendre publique, ce qui n'est pas du tout satisfaisant du point de vue de la conception: cette fonction est protégée car elle n'est qu'un utilitaire à la classe, il n'y a aucune raison que tout le monde puisse l'utiliser.
Une autre solution est de laisser la fonction protégée mais de déclarer la classe Parchemin amie de la classe arme. Cette solution n'est pas plus satisfaisante que la précédente car à chaque création d'un nouveau décorateur, il faudrait modifier la classe Arme, ce qui viole le principe d'ouverture/fermeture qui affirme qu'une classe doit être fermée aux modifications mais ouverte aux évolutions.
La solution se trouve un étage au dessus. Il suffit de créer une fonction protégée degat_arme dans la classe Sort qui renverra les dégâts de l'arme de base et de déclarer cette fonction amie de la classe. On se retrouve donc avec le code suivant:
// On ne fait que compléter le code vu avant.
class
Arme
{
//...
protected
:
friend
class
Sort;
}
;
class
Sort: public
Arme
{
//...
protected
:
int
degat_arme(Personnage&
p) const
{
return
marme->
calculer_degat(p);}
}
;
template
<
class
Modificateur>
class
Parchemin: public
Sort
{
//...
protected
:
virtual
int
calculer_degat(Personnage&
p) const
{
return
Modificateur::
calculer(degat_arme(p));
}
}
;
Avec cette structure, la fonction calculer_degat n'est pas exposée publiquement et on peut créer de nouveaux décorateurs sans soucis. L'encapsulation est respectée et le principe de fermeture aussi.
V-5. Remarques▲
L'utilisation des boost::shared_ptr permet de créer dynamiquement des objets pour les décorateurs sans se soucier de la destruction. Si vous aviez voulu passez des objets alloués sur la pile aux décorateurs, il aurait fallu utiliser des pointeurs bruts et ne faire aucune destruction dans les différents destructeurs. Néanmoins, cette approche est plus problématique car on perd au fil du code les responsabilité : qui doit détruire quoi et à quel moment ?
Une seconde remarque est pour l'annulation des décorateurs. Pour annuler les décorateurs, il suffit de récupérer l'objet sous-jacent via une fonction et de détruire ensuite le décorateur. On se retouve alors avec une structure de pile LIFO (last in, first out).