IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Présentation des principaux design patterns en C++


précédentsommairesuivant

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:

Diagramme UML du DP décorateur
Diagramme UML du pattern décorateur

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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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:

 
Sélectionnez
//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

 
Sélectionnez
//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.

 
Sélectionnez
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.

 
Sélectionnez
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:

 
Sélectionnez
// 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).


précédentsommairesuivant

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2007 Côme David. Aucune reproduction, même partielle, ne peut être faite de ce site ni 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.