V. Le décorateur▲
Définition ; Les décorateurs sont l'ensemble des classes permettant d'étendre dynamiquement le rôle d'une classe de base.
V-1. Pourquoi le décorateur▲
Imaginez que vous êtes en train de coder un programme qui gère des commandes de nourriture dans un café : des gaufres, des crêpes avec du sucre, du nutella et qu'en plus, ce café gère un stock de fromages.
Comment modéliser ces objets ?
On pourrait créer autant de classes qu'il y a de possibilités, mais ceci est fort gênant puisqu'il va y avoir une explosion du nombre de classes et une redondance inimaginable au niveau du code.
Pour résoudre ce problème, on préfère le pattern décorateur. Grâce à lui, vous pourrez étendre de façon dynamique et non intrusive le rôle d'une classe.
Pour cela, on va wrapper l'objet à décorer avec les décorateurs.
Mais pour continuer à utiliser notre objet de base sans changement, il faut que toutes les classes dérivent d'un même composant de base.
On en déduit donc le diagramme suivant :
V-2. Implémentation première des décorateurs▲
Avec l'analyse précédente, on arrive au code suivant :
#ifndef DECO_H
#define DECO_H
#include
<string>
#include
<iostream>
//==========================================================================================
class
BaseAliment
{
protected
:
//les données de la classe de base
double
m_prix;
std::
string m_name;
virtual
~
BaseAliment()=
0
;
BaseAliment(double
prix=
0
,const
std::
string&
name=
""
);
public
:
virtual
double
GetPrix() const
;
virtual
std::
string GetName() const
;
virtual
void
Afficher(std::
ostream &
flux) const
;
}
;
class
Aliment : public
BaseAliment
{
public
:
Aliment(double
prix=
0
,const
std::
string&
name=
""
);
virtual
~
Aliment();
}
;
//==========================================================================================
//l'aliment à proprement parler. ici trivial.Il pourrait être substitué par BaseAliment
class
Decorateur : public
BaseAliment
{
protected
:
BaseAliment&
m_base;
Decorateur(BaseAliment&
base,double
prix=
0
,const
std::
string&
name=
""
);
virtual
~
Decorateur() =
0
;
public
:
BaseAliment*
GetBase() const
{
return
&
m_base;}
;
}
;
//un premier décorateur permettant d'ajouter du goût à un aliment
//Ce décorateur modifie les fonctions déjà présentes
class
DecorateurGout : public
Decorateur
{
public
:
double
GetPrix() const
;
std::
string GetName() const
;
DecorateurGout(BaseAliment&
base,double
prix,const
std::
string&
name);
~
DecorateurGout();
}
;
//un deuxième décorateur permettant d'ajouter une date limite à un aliment
//Ce décorateur ajoute une nouvelle fonction.
class
DecorateurPerissable : public
Decorateur
{
std::
string m_limite;
public
:
std::
string GetLimite() const
{
return
(m_limite) ;}
void
Afficher(std::
ostream &
flux) const
;
DecorateurPerissable(BaseAliment&
base,const
std::
string&
limite);
~
DecorateurPerissable(){}
}
;
#endif
Et le fichier décorateur.cpp:
#include
"deco.h"
using
namespace
std;
BaseAliment::
~
BaseAliment()
{
}
Aliment::
~
Aliment()
{
}
DecorateurGout::
~
DecorateurGout()
{
}
Decorateur::
~
Decorateur()
{
}
BaseAliment::
BaseAliment(double
prix,const
std::
string&
name):
m_prix(prix),m_name(name)
{
}
Aliment::
Aliment(double
prix,const
std::
string&
name):
BaseAliment(prix,name)
{
}
Decorateur::
Decorateur(BaseAliment&
base,double
prix,const
std::
string&
name):
BaseAliment(prix,name),m_base(base)
{
}
DecorateurGout::
DecorateurGout(BaseAliment&
base,double
prix,const
std::
string&
name):
Decorateur(base,prix,name)
{
}
DecorateurPerissable::
DecorateurPerissable(BaseAliment&
base,const
std::
string&
limite):
Decorateur(base),m_limite(limite)
{
}
double
BaseAliment::
GetPrix() const
{
return
m_prix;
}
std::
string BaseAliment::
GetName() const
{
return
m_name;
}
void
BaseAliment::
Afficher(std::
ostream &
flux) const
{
flux<<
GetName() <<
" :"
<<
GetPrix();
}
double
DecorateurGout::
GetPrix() const
{
return
(m_base.GetPrix()+
m_prix);
}
std::
string DecorateurGout::
GetName() const
{
return
(m_base.GetName()+
" "
+
m_name);
}
void
DecorateurPerissable::
Afficher(std::
ostream &
flux) const
{
m_base.Afficher(flux);
flux<<
" "
<<
m_limite;
}
std::
ostream &
operator
<<
(std::
ostream &
flux,const
BaseAliment&
t)
{
t.Afficher(flux);
return
flux;
}
int
main(int
argc,char
*
argv[])
{
BaseAliment *
c=
new
Aliment(1.5
,"Gauffre"
);
cout<<
(*
c)<<
endl;
c=
new
DecorateurGout(*
c,0.2
,"Sucre"
);
cout<<*
c<<
endl;
c=
new
DecorateurGout(*
c,0.3
,"Nutella"
);
cout<<*
c<<
endl;
cout<<
endl;
BaseAliment *
d=
new
Aliment(2.0
,"Fromage"
);
cout<<
(*
d)<<
endl;
d=
new
DecorateurGout(*
d,15
,"Puant"
);
cout<<
(*
d)<<
endl;
d=
new
DecorateurPerrisable(*
d,"31/12/07"
);
cout<<
(*
d)<<
endl;
}
Ce code est moins facile que le précédent.
Tout d'abord le destructeur virtuel pur avec une définition. Essayons de comprendre pourquoi il en est ainsi.
Avant toute chose, il faut remarquer que BaseAliment est ici une interface. Pour l'empêcher d'être instanciée il faut donc qu'elle possède une fonction membre virtuelle pure.
Mais le seul problème, c'est que toutes les fonctions membres ont une définition de base.
On pourrait ajouter une fausse fonction membre abstraite mais cela serait du toc et une grave faute de conception.
Le choix du destructeur est alors un bon choix. Grâce à lui, vous pouvez disposer d'un destructeur qui effectue son rôle puisqu'il est bien défini tout en ayant une classe abstraite.
Ensuite, le pattern en lui-même. Il se base sur le mélange héritage/composition.
Pour cela, on définit tout d'abord une classe de base (ici BaseAliment) qui fournit aux autres classes une interface à respecter.
Ensuite, à partir de cette classe, on définit un composant (Aliment) et une classe décorateur qui va servir d'interface à tous les décorateurs.
Puis à partir de la classe Décorateur, on crée de nouvelles classes qui représentent le décorateur concret.
Chaque décorateur concret offre une nouvelle fonction à l'objet qu'il reçoit en paramètre dans son constructeur.
Cette nouvelle fonction peut être soit une extension d'une fonction existante (ajouter un nouveau goût pour DecorateurGout) ou créer une nouvelle fonction pour DécorateurLimite.
Dans le cas où la décoration modifie une fonction préexistante, elle s'exécute toujours en appelant la fonction de l'objet décoré et en ajoutant un calcul avant ou après celle-ci.
En pratique, on chaîne souvent les constructions comme suit, bien que cela puisse réduire la lisibilité :
BaseAliment *
c=
new
DecorateurGout( *
(new
Aliment(1.5
,"Gauffre"
)),0.2
,"Sucre"
) ;
V-3. Remarques sur le décorateur▲
V-3-a. Explosion du nombre de classes▲
Bien que le nombre de classes soit moins important qu'avec la méthode exhaustive le nombre de classes reste quand même assez conséquent. Pour palier ce problème on couple souvent le décorateur avec une fabrique de telle sorte que l'on ne crée quasiment plus rien à la main.
V-3-b. Pas de template▲
Le décorateur est l'un des rares patterns qui ne se template pas facilement. En effet, les patterns précédents offraient des points invariants quel que soit l'implémentation. Or ici, le décorateur n'offre pas ces points invariants puisqu'il présente plutôt un concept. Il est donc beaucoup plus difficile à mettre sous forme de template.