Présentation des principaux design patterns en C++

Come David

Cet article a pour but de vous présenter la majorité des design patterns via un exemple de besoin qui sera complété par une mise en pratique en C++.

- Titre : Présentation des principaux design patterns en C++

- Auteur : Come David

- Parution : 20 février 2008

- Mise à jour : 6 juin 2020

- Licence : 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 et 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.

Sommaire

- I. Introduction

- II. Prototype

- II-A. Exemple de besoin du protoype

- II-B. Exemple naïf d'implémentation

- II-C. Implémentation avec les templates

- III. Le singleton

- III-A. Quand a-t-on besoin du singleton ?

- III-B. Exemple simpliste d'implémentation du Singleton

- III-C. Implémentation templatisée

- III-D. Remarques sur le singleton

- III-D-1. Le singleton non thread-safe

- III-D-1-a. Première solution envisageable

- III-D-1-b. Problème lié à cette solution

- III-D-1-c. Problème lié au double check

- III-D-2. Le singleton est une variable globale

- IV. La fabrique

- IV-A. Un besoin de fabrique

- IV-B. Première implémentation

- IV-C. Un code plus réutilisable avec les templates

- V. Le décorateur

- V-A. Pourquoi le décorateur

- V-B. Implémentation première des décorateurs

- V-C. Remarques sur le décorateur

- V-C-1. Explosion du nombre de classes

- V-C-2. Pas de template

- VI. Le composite

- VI-A. Quand a-t-on besoin de celui-ci ?

- VI-B. Première implémentation

- VI-C. Pas de templates

- VII. L'observateur

- VII-A. Pourquoi observer ?

- VII-B. Première implémentation

- VII-C. Remarque sur ce pattern

- VII-C-1. Tirer/Pousser

- VIII. La stratégie

- VIII-A. Pourquoi adopter la stratégie

- VIII-B. Première implémentation

- VIII-C. Remarques sur la stratégie

- VIII-C-1. Parallèle avec les politiques

- IX. Conclusion

- IX-A. Que retenir de mon article ?

- IX-B. Références

- IX-C. Remerciements

I - Introduction

Les design patterns. Un nom récurrent quand on code un minimum. On en a tous entendu parler de ces patterns, mais que sont-ils ?

Ce sont des modèles théoriques adaptables qui résolvent un problème précis.

Je vais vous montrer dans la suite de cet article les principaux patterns accompagnés à chaque fois d'une implémentation en C++.

II - Prototype

Définition : un prototype est une classe dont le but est d'être clonée.

II-A - Exemple de besoin du protoype

Pendant la création de vos programmes, vous serez sans doute amenés à instancier des objets de taille conséquente en mémoire.
Créer un gros objet ne pose guère de problèmes. Mais en créer plusieurs à la suite peut amener à tuer les performances de votre application.
La solution est alors de copier l'objet de base, le prototype, puis de modifier ce qui doit l'être pour que le nouvel objet réponde aux besoins.

une autre utilisation est la possibilité qu'offre le clonage pour dupliquer des objets polymorphes. En effet pour utiliser le constructeur de copie, il faut connaître le type réel de l'objet, ce qui n'est plus le cas dès qu'on utilise le polymorphisme de dynamique en C++.

De cette façon, on s'aperçoit que le pattern prototype dispose d'une méthode obligatoire, Clone, et d'une facultative Change. La première a pour but d'effectuer une copie du prototype tandis que la seconde a pour rôle de changer l'état de l'objet.
Ainsi, le diagramme UML qui modélise le pattern prototype est :

Diagramme UML du pattern prototype

II-B - Exemple naïf d'implémentation

D'après l'analyse précédente et en prenant comme exemple une connexion Mysql, une première implémentation de ce pattern pourrait être tout simplement :

//fichier prototype.h

#ifndef PROTOTYPE_H
#define PROTOTYPE_H

#include <string>

class Prototype
{
public:
virtual ~Prototype();
virtual Prototype* Clone() const = 0;
};

class MySQLManager: public Prototype
{
std::string m_host,m_login,m_pass;
std::string m_base,m_table;
public:
MySQLManager(const std::string& host="",const std::string& login="",const std::string& pass="");
MySQLManager* Clone() const;

void Afficher();
void Set(const std::string& base="",const std::string& table="");
};

#endif

Et dans le fichier prototype.cpp

#include <iostream>
#include "prototype.h"

Prototype::~Prototype()
{

}

MySQLManager::MySQLManager(const std::string& host,const std::string& login,const std::string& pass):
m_host(host),m_login(login),m_pass(pass)
{
//on se connecte à la base Mysql.
//ceci prend du temps, car il faut se connecter puis s'identifier.
//Lourd.
}

MySQLManager* MySQLManager::Clone() const
{
// le constructeur de recopie fait tout le travail à notre place.
return (new MySQLManager(*this));
}

void MySQLManager::Set(const std::string& base,const std::string& table)
{
m_base = base;
m_table = table;
//on se connecte a la base m_base pour étudier m_table
}
void MySQLManager::Afficher() const
{
std::cout<<m_host<<"|"<<m_login<<"|"<<m_host<<"|"<<m_pass<<"|"<<m_base<<"|"<<m_table<<std::endl;
}

int main(void)
{
MySQLManager manager1("localhost","Davidbrcz","motdepasse");
manager1.Set("faussebase","table1");
//ici manager1 est connecté sur localhost avec l'identifiant Davidbrcz sur la table table1 de faussebasse

manager1.Afficher();

//Maintenant, on doit travailler en parallèle sur table2 de faussebase2 toujours sur localhost.
//on crée donc un autre manager qu'on récupère avec un std::auto_ptr pour éviter de gérer la mémoire en cas de problème.
std::auto_ptr<MySQLManager> manager2(manager1.Clone());
//et ici manager2 est déjà connecté à localhost avec l'identifiant Davidbrcz.Pas besoin de se réidentifier.
manager2->Afficher();
manager2->Set("faussebase2","table2");
manager2->Afficher();

//on travaile ..

return 0;
}

Ce code mérite quelques explications.
Tout d'abord on déclare une classe Prototype qui est une interface grâce à la méthode virtuelle pure Clone. Ensuite on déclare une classe MySQLManager qui hérite de Prototype.

On rend cette classe instanciable en définissant la méthode Clone. Mais la fonction virtuelle Prototype::Clone renvoie un Prototype. Or le type de retour ne rentre normalement pas dans la définition d'une fonction. Le compilateur devrait donc en théorie me crier dessus.

Mais si ceci compile sans problème c'est grâce aux valeurs de retour covariantes.
Pour faire simple les valeurs de retour covariantes sont les cas où la valeur de retour d'une fonction virtuelle est une référence ou un pointeur sur une classe C.Ainsi les classes qui redéfiniront cette fonction virtuelle pourront renvoyer un pointeur ou une référence sur une classe dérivée de C.

C'est ce qu'il se passe ici. MySQLManager dérive de Prototype donc MySQLManager::Clone peut renvoyer un pointeur vers une instance de MySQLManager.
Mais le problème ici, c'est que Clone renvoie un pointeur brut, utiliser un pointeur intelligent pour le gérer n'est pas du tout obligatoire. Si l'utilisateur utilise un pointeur brut pour gérer la mémoire, ceci peut poser des problèmes pour récupérer la mémoire en cas d'exception.

II-C - Implémentation avec les templates

Les retours covariants ne marchent pas avec des pointeurs intelligents. Pour contourner cela, on doit utiliser des templates
Ainsi les classes Prototype et MySQLManager deviennent :

//fichier prototypetpl.h

#ifndef PROTOTYPE_H
#define PROTOTYPE_H

#include <string>
#include <memory>

template <class T> class Prototype
{
public:
virtual ~Prototype(){}
virtual std::auto_ptr<T> Clone() const = 0;
};

class MySQLManager: public Prototype<MySQLManager>
{

std::string m_host,m_login,m_pass;
std::string m_base,m_table;

public:
MySQLManager(const std::string host="",const std::string login="",const std::string pass="");

void Afficher() const ;
std::auto_ptr<MySQLManager> Clone() const ;
void Set(const std::string base="",const std::string table="");
};

#endif

Et le fichier prototype.cpp (seul change Clone, le reste est identique)

std::auto_ptr<MySQLManager> MySQLManager::Clone() const
{
return std::auto_ptr<MySQLManager>(new MySQLManager(*this));
}

int main(void)
{
//main ne change pas, on est juste sûr que la mémoire sera libérée en cas de problème
}

L'utilisation des templates permet de simuler des retours covariants avec les pointeurs intelligents. Ceci assure une plus grande résistance au programme.

III - Le singleton

Définition : le singleton permet de s'assurer qu'il n'existe qu'une unique instance d'une classe donnée.

III-A - Quand a-t-on besoin du singleton ?

Lors du développement de vos logiciels, vous souhaiterez sans doute vous assurer de n'avoir qu'une seule instance de certaines classes. En effet, imaginez que dans un jeu vidéo, un manager de son est, par mégarde, créé deux fois. Ceci pose de gros problèmes sur le plan de la gestion de la mémoire.

La solution est alors d'utiliser le pattern singleton. En effet celui-ci va, via une de ses méthodes, vous permettre de récupérer l'unique instance de la classe.

De cette analyse, on tire le diagramme suivant :

Diagramme UML du pattern singleton

III-B - Exemple simpliste d'implémentation du Singleton

Un premier exemple pour implémenter le pattern singleton peut tout simplement être le suivant (avec comme choix d'unique objet un manager de son). Le fichier singleton.h

#ifndef SINGLETON_H
#define SINGLETON_H

class SoundManager
{
public:
static SoundManager& Instance();
private:
SoundManager& operator= (const SoundManager&){}
SoundManager (const SoundManager&){}

static SoundManager m_instance;
SoundManager();
~SoundManager();
};
#endif

Et dans le fichier singleton.cpp

#include <iostream>
#include "singleton.h"

using namespace std;

SoundManager SoundManager::m_instance = SoundManager();

SoundManager::SoundManager()
{
cout<<"Creation"<<endl;
}

SoundManager::~SoundManager()
{
cout<<"Destruction"<<endl;
}

SoundManager& SoundManager::Instance()
{
return m_instance;
}

int main(void)
{
//1er appel de Instance: on alloue le pointeur SoundManager::m_instance
SoundManager& ptr1 = SoundManager::Instance();

//2eme appel:on se contente de renvoyer le pointeur déjà allouer.
SoundManager& ptr2 = SoundManager::Instance();

//ptr1 et ptr2 pointe sur la même adresse mémoire.
//On voit donc qu'il n’y a bien qu'un seul objet.
cout<<&ptr1<<"|"<<&ptr2<<endl;

return 0;
}

Ce code ne présente pas de problème sur le plan de la syntaxe. Par contre, il soulève une question sur le plan des performances.

Que se passe-t-il si Intance n'est jamais appelée ? Personne ne va utiliser SoundManager. Néamoins, un objet est quand même créé ! Utiliser un objet statique assure que le singleton est thread-safe, par contre il y forcément construction d'un objet qui peut ne pas être utilisé. Pour éviter cela, on peut utiliser un pointeur statique, alloué dans Instance et détruit dans une fonction membre Kill. Le code n'est plus thread-safe (cf 4-a), mais l'objet n'est construit que si on l'utilise.

III-C - Implémentation templatisée

Ce singleton fonctionne bien, mais il est un peu lourd à écrire. En effet avec Instance fait la même chose d'une classe à l'autre aux types près, les templates semblent s'imposer d'elles-mêmes.
Ainsi le code devient dans le fichier singleton.h :

#ifndef SINGLETONTPL_H
#define SINGLETONTPL_H

template <class T> class Singleton
{

public:
static T& Instance();
protected:
static T m_i;

private:
T& operator= (const T&){}
};

class SoundManager :public Singleton<SoundManager>
{
friend class Singleton<SoundManager>;

private:

SoundManager(const SoundManager&){}
SoundManager();
~SoundManager();
};

#include "singleton.inc"
#endif

Et dans le fichier singleton.inc

#include <iostream>
#include "singletontpl.h"

template <class T> T Singleton<T>::m_i = T();

template <class T>  T& Singleton<T>::Instance()
{
return m_i;
}

SoundManager::SoundManager()
{
std::cout<<"Creation"<<std::endl;
}

SoundManager::~SoundManager()
{
std::cout<<"Destruction"<<std::endl;
}

int main(void)
{
SoundManager& sin = Singleton<SoundManager>::Instance();
SoundManager& sin2 = Singleton<SoundManager>::Instance();
std::cout<<&sin<<"|"<<&sin2<<std::endl;
}

La seule innovation de ce code vis-à-vis du précédent est l'héritage particulier de SoundManager

En effet, avec la classe singleton actuelle, il faut la faire hériter de la classe que l'on souhaite avoir en un unique exemplaire. Or la classe Singleton est template, le seul moyen de l'instancier est donc de passer la classe dérivée en paramètre de Singleton.

Le seul problème lié à ce code est que m_i doit être initialisé avec un objet de type T, ce qui implique d'appeler le constructeur de T, qui est privé pour garantir l'unicité de T
Ce problème est résolu via l'utilisation de l'amitié sur la classe Singleton dans la classe dérivée.

III-D - Remarques sur le singleton

III-D-1 - Le singleton non thread-safe

Voici le singleton non thread safe mais qui utilise la lazy initialisation

#ifndef SINGLETONTPL_H
#define SINGLETONTPL_H

template <class T> class Singleton
{
public:
static T* Get();
static void Kill();
protected:
static T* m_i;
private:
T& operator= (const T&){}
};

class SoundManager :public Singleton<SoundManager>
{
friend SoundManager* Singleton<SoundManager>::Get();
friend void Singleton<SoundManager>::Kill();

private:
SoundManager (const SoundManager&){}

SoundManager();
~SoundManager();
};

#endif
//main.cpp

template <class T> T* Singleton<T>::m_i = 0;

template <class T>  T* Singleton<T>::Get()
{
if(m_i == 0)
{
m_i = new T();
}
return m_i;
}

template <class T> void Singleton<T>::Kill()
{
delete m_i;
m_i = 0;
}

SoundManager::SoundManager()
{
std::cout<<"Création"<<std::endl;
}

SoundManager::~SoundManager()
{
std::cout<<"Destruction"<<std::endl;
}

int main(void)
{
SoundManager* sin = Singleton<SoundManager>::Get();
SoundManager* sin2 = Singleton<SoundManager>::Get();
std::cout<<sin<<"|"<<sin2<<std::endl;
Singleton<SoundManager>::Kill();
}

III-D-1-a - Première solution envisageable

Le singleton que je vous propose fonctionne bien, mais n'est absolument pas thread-safe.

En effet, imaginez que dans un contexte multi-thread un thread A exécute jusqu'à la ligne if(m_i==0) de Get puis qu'il soit suspendu par l'OS.
Tout ce que nous pouvons dire pour le moment, c'est qu'aucun objet de type Singleton n'a été créé, mais qu'à la reprise du thread A, un objet va être créé.

Si juste après la suspension de A un autre thread (nommé B) exécute entièrement Get, le Singleton sera créé. Mais lorsque A reprendra son exécution, il va continuer et créer un autre Singleton.
Ce qui fait que l'on se retrouve avec deux objets de type Singleton ! Ce qui est fondamentalement contraire à ce pattern.

Une première solution est de placer un mutex (objet qui permet de bloquer l'accès à des variables se situant après sa création tant que ce premier n'a pas été détruit) dans la méthode Get juste avant if(m_i==0). De cette façon, le code de Get devient :

template <class T>  T* Singleton<T>::Get()
{
Lock lock //ici Lock est une classe symbolique. Elle représente un mutex.
if(m_i == 0)
{
m_i = new T();
}
return m_i;
}

III-D-1-b - Problème lié à cette solution

Mais cette solution est très mauvaise. En effet nous avons besoin de locker seulement à la première création de m_instance.
Alors pourquoi payer le prix d'un lock à chaque appel de Get quand un seul est nécessaire ?

C'est pour résoudre ce problème que l'on va utiliser le principe du double check. En effet, pourquoi acquérir un lock si le singleton a déjà été créé ?
Ainsi selon cette courte réflexion, la méthode Get devient :

template <class T>  T* Singleton<T>::Get()
{
if(m_i==0) //Premier check
{
Lock lock
if(m_i == 0) //Second
{
m_i = new T();
}
}
return m_i;
}

Et de ce fait le nom de double check prend tout son sens !

En effet, le premier sert à vérifier que le singleton n'a pas déjà été créé, auquel cas on ne fait que renvoyer l'instance.
Et le second sert à s'assurer que le singleton n'a pas été créé par un autre thread entre le premier test et le moment d'acquisition du lock.

III-D-1-c - Problème lié au double check

Dans cette sous-section, je présume que vous disposez de connaissances du C++ assez évoluées, tel que le placement new.

Pour entrevoir le problème, il faut descendre plus bas dans le langage.
Avant toute chose, regardons comment se décompose une allocation dynamique de type T pour un pointeur p :

1. Réservation de mémoire de taille suffisamment grande pour contenir un objet de type T.

2. Appel du constructeur de T.

3. Affectation de l'adresse réservée par la première opération à p.

Ce qui fait que l'on peut écrire notre singleton de cette façon :

template <class T>  T* Singleton<T>::Get()
{
if(m_i == 0)  //1er check
{
Lock lock;
if(m_i == 0)  //2eme
{
m_i =                                       // phase 3 de la liste
static_cast<T*>(operator new(sizeof(T))); // phase 1
new (m_i) T;                                // phase 2
}
}
return m_i;
}

Ce code, s'il est exécuté dans cet ordre, ne pose aucun problème.
Mais comme la vie n'est pas rose, les compilateurs peuvent, s'ils le souhaitent, intervertir les phases (iii) et (ii) de telle sorte que l'adresse mémoire soit affectée à m_i avant l'appel du constructeur. Or ceci pose un problème. Imaginons le scénario suivant :

1. Un thread A appelle Get, obtient un lock et effectue les étapes (i) et (iii) avant d'être suspendu.
Au moment de sa suspension, m_i n'est pas NULL, mais ne pointe pour autant pas sur un objet valide.

2. Un thread B appelle à son tour Get. À ce moment, cet appel de Get ne passe pas le premier check car m_i n'est justement pas NULL !
Puis il renvoie l'instance non complètement construite de m_i. À l'instant où le code client va déférencer m_i, boom !

En effet, c'est là que se pose tout le problème du double check, puisqu'il impose un ordre de séquencement que les compilateurs ne sont pas obligés de respecter.
Quoi que vous fassiez,vous ne pourrez pas obliger le compilateur à suivre l'ordre de séquencement qui vous arrange, ce qui fait que pour certaines personnes, en contexte multi-thread, le pattern singleton peut être considéré comme un anti-pattern.

III-D-2 - Le singleton est une variable globale

Je tiens à vous mettre en garde, le singleton est malgré son apparence une variable globale ! Il est donc à utiliser avec précaution. De plus, faites attention à ne pas attraper la singletonite-aiguë qui consiste à mettre des singletons un peu partout. Si vous ne devez construire qu'une seule instance dans un objet dans un petit programme mono-thread, ne sortez pas le singleton en prétextant le fait qu'il ne doit y avoir qu'une seule instance. S'il ne doit en avoir qu'une dans votre programme, il vous suffit de n'en créer qu'une !

IV - La fabrique

Fabrique : classe dont le rôle est de créer d'autres objets.

IV-A - Un besoin de fabrique

Il est courant en C++ d'avoir comme design une hiérarchie de classes dérivant toutes d'une même interface pour profiter du polymorphisme. Mais gérer manuellement la création d'objets peut vite être fatigant. La solution est alors de se reposer sur la fabrique qui, comme son nom l'indique, va fabriquer des objets.

Pour fabriquer, ou créer, des objets, elle va disposer d'une méthode Create prenant en paramètre le type d'objet à créer et d'une méthode Register pour associer à chaque type à créer une "action". On en tire donc le diagramme suivant :

Diagramme UML de la fabrique

IV-B - Première implémentation

Avant toute chose, il faut préciser comment seront créés les objets. En effet, sur ce point, plusieurs stratégies sont envisageables telle la création via new ou celle par clonage. Pour ma part, j'ai choisi celle basée sur le clonage. Mais vous trouverez facilement d'autres implémentations sur le web.

D'après l'analyse et la remarque précédente, on peut écrire une première version de la fabrique comme suit :
fabrique.h

#ifndef FABRIQUE_H
#define FABRIQUE_H

#include <map>
#include <string>

//la classe prototype abordé en II
template <class T> class Prototype
{
public:
virtual ~Prototype(){}
virtual T* Clone() const = 0;
};

//l'interface "Figure"
class Figure : public  Prototype<Figure>
{
public:
virtual void SeDessiner() = 0;
};

class Carre : public Figure
{
public:
Figure* Clone() const;
void SeDessiner();

};

class Cercle : public Figure
{
public:
Figure* Clone() const ;
void SeDessiner();
};

//La fabrique à proprement parler
class FactoryFigure
{
public:
static std::map<std::string,Figure*> m_map;

public:
//Fonction qui associe clé <=> prototype
static void Register(const std::string& key,Figure* obj);

//Celle qui va créer les objets
Figure* Create(const std::string& key) const;
};
#endif

Et le fichier fabrique.cpp

#include <iostream>
#include <iterator>

#include "fabrique.h"

using namespace std;

Figure* Carre::Clone() const
{
return new Carre(*this);
}

void Carre::SeDessiner()
{
cout<<"Je suis un Carre"<<endl;
}

Figure* Cercle::Clone() const
{
return new Cercle(*this);
}

void Cercle::SeDessiner()
{
cout<<"Je suis un Cercle"<<endl;
}

std::map<string,Figure*> FactoryFigure::m_map = std::map<string,Figure*>();

void FactoryFigure::Register(const string& key,Figure* obj)
{
//si la clé n'est pas déjà présente
if(m_map.find(key) == m_map.end())
{
//on ajoute l'objet dans la map
m_map[key] = obj;
}

//on pourrait détruire obj, mais cette tâche ne revient pas à Register
}

Figure* FactoryFigure::Create(const std::string& key) const
{
Figure* tmp = 0;
std::map<string, Figure*>::const_iterator it = m_map.find(key);

//si l'itérateur ne vaut pas map.end(), cela signifie que la clé à été trouvée
if(it != m_map.end())
{
tmp = ((*it).second)->Clone();
}

//on pourrait lancer une exception si la clé n'a pas été trouvée

return tmp;
}

int main(void)
{
//notre fabrique
FactoryFigure fac;

//on enregistre des types
FactoryFigure::Register("Carre",new Carre);
FactoryFigure::Register("Cercle",new Cercle);

//on crée des objets via la fabrique
Figure *c = fac.Create("Cercle");
Figure *ca = fac.Create("Carre");

c->SeDessiner();
ca->SeDessiner();

delete c;
delete ca;

return 0;
}

Ce code ne présente aucune grande difficulté. Pour chaque clé, on associe un prototype que la méthode Create se charge de cloner, rien de plus.

Mais ce code est très mauvais du point de vue de la conception. En effet, la fabrique est intimement liée au type d'objet qu'elle va créer. De ce fait, pour créer une autre fabrique, il faut copier-coller du code ce qui est très mauvais signe.

IV-C - Un code plus réutilisable avec les templates

Comme on l'a vu, une fabrique est liée à deux choses : le type d'objet qu'elle va créer et la clé qu'elle va utiliser pour les indexer. De ce fait, il 'suffit' de transformer Figure et std::string en deux paramètres pour une classe template afin d'obtenir une très grande modularité.

En effet, avec les templates, on peut indexer n'importe quel type d'objet avec n'importe quoi (des ints, des strings...). De ce fait, le code de la fabrique devient :

//fichier fabrique.h
template <class Object,class Key=string> class Factory
{
static std::map<Key,Object*> m_map;
public:

static void Register(Key key,Object* obj);
Object* Create(const Key& key);
};

//fichier fabrique.cpp
template <class Object,class Key> std::map<Key,Object*> Factory<Object,Key>::m_map = std::map<Key,Object*>();

template <class Object,class Key> void Factory<Object,Key>::Register(Key key,Object* obj)
{
if(m_map.find(key) == m_map.end())
{
m_map[key] = obj;
}
}

template <class Object,class Key> Object* Factory<Object,Key>::Create (const Key& key)
{
Object* tmp = 0;
typename std::map<Key, Object*>::iterator it = m_map.find(key);

if(it != m_map.end())
{
tmp = ((*it).second)->Clone();
}

return tmp;
}

Et son utilisation est comme celle d'un vecteur :

Fabrique<Figure> fac;

La seule vraie innovation est la présence du typename devant l'itérateur. Pour sa justification, reportez-vous à la FAQ. En ce qui concerne l'initialisation de m_map, elle n'a pas changé, elle est juste passée à la version template qui fait un peu plus peur, mais qui est exactement la même qu'avant, à la généricité près.

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-A - 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 :

Diagramme UML du pattern décorateur

V-B - 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-C - Remarques sur le décorateur

V-C-1 - 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 pallier 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-C-2 - 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, quelle 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.

VI - Le composite

Définition : Le composite est un pattern qui permet de manipuler un ensemble d'objets comme un seul.

VI-A - Quand a-t-on besoin de celui-ci ?

Hors du code, le pattern composite est l'un des patterns que l'on croise le plus souvent sans s'en rendre compte puisque dès que l'on navigue dans l'arborescence d'un disque dur, nous le côtoyons. En effet, quand nous naviguons, nous pouvons manipuler un dossier comme bon nous semble, quoi que contienne ce dossier (autres dossiers, fichiers...). Et c'est précisément ce que propose le composite : pouvoir s'abstraire du nombre d'objets réels et de les manipuler comme s'il n'y en avait qu'un.

De ce fait le composite va tout d'abord fournir une interface pour permettre de manipuler les objets, puis deux classes qui héritent de cette interface. Une qui représente un objet 'terminal' et une autre pour représenter l'objet composite en lui même.

De ce fait, le diagramme UML du pattern est :

Diagramme UML du pattern composite

VI-B - Première implémentation

En reprenant l'exemple de l'arborescence d'un disque dur sous un système unix, un premier code peut être :

#ifndef COMPOSITE_H
#define COMPOSITE_H

#include <string>
#include <vector>

//un fichier de base
class FichierAbstrait
{
static const int m_taille;

protected:
std::string m_nom;

public:
virtual int GetTaille() const;
virtual void Afficher() const ;

virtual ~FichierAbstrait() = 0;
FichierAbstrait(const std::string& nom);
};

//un fichier terminal
class Fichier :public FichierAbstrait
{
private:
std::string m_type;

public:
void Afficher() const ;
Fichier(const std::string& nom,const std::string& type);
~Fichier();
};

//un conteneur
class Dossier : public FichierAbstrait
{
private:
std::vector<FichierAbstrait*> m_listfichier;

public:

void Afficher() const;
int GetTaille() const;

Dossier& Add(Dossier* file);

void Add(Fichier* file);

Dossier(const std::string& path);
~Dossier();

};

#endif

Et dans le fichier composite.cpp

#include <iostream>
#include <string>
#include <iterator>

#include "composite.h"

using namespace std;

const int FichierAbstrait::m_taille = 1;
//=======
FichierAbstrait::FichierAbstrait(const std::string& nom):
m_nom(nom)
{

}

int FichierAbstrait::GetTaille() const
{
return m_taille;
}

void FichierAbstrait::Afficher() const
{
cout<<m_nom<<" "<<GetTaille();
}

FichierAbstrait::~FichierAbstrait()
{

}
//=======
Fichier::Fichier(const string& nom,const string& type):
FichierAbstrait(nom),m_type(type)
{

}
void Fichier::Afficher() const
{
FichierAbstrait::Afficher();cout<<" "<<m_type<<endl;
}

Fichier::~Fichier()
{

}
//=======
Dossier::Dossier(const string& path):FichierAbstrait(path)
{

}

void Dossier::Afficher() const
{
//pour chaque élément du vecteur, on l'affiche
vector<FichierAbstrait*>::const_iterator itb = m_listfichier.begin();
const vector<FichierAbstrait*>::const_iterator ite = m_listfichier.end();

cout<<"["<<m_nom<<"]";
for(; itb != ite; itb++)
{
(*itb)->Afficher();
}
}

int Dossier::GetTaille() const
{
//on fait la somme de la taille de chaque élément
int somtaille = 0;

vector<FichierAbstrait*>::const_iterator itb = m_listfichier.begin();
const vector<FichierAbstrait*>::const_iterator ite = m_listfichier.end();

for(; itb != ite; itb++)
{
somtaille += (*itb)->GetTaille();
}

return somtaille;
}

Dossier::~Dossier()
{
//on détruit chaque élément
vector<FichierAbstrait*>::const_iterator itb = m_listfichier.begin();
const vector<FichierAbstrait*>::const_iterator ite = m_listfichier.end();

for(; itb != ite; itb++)
{
delete (*itb)
}
}

Dossier& Dossier::Add(Dossier* file)
{
m_listfichier.push_back(file);
return (*file);
}

void Dossier::Add(Fichier* file)
{
m_listfichier.push_back(file);
}
//=======
int main(void)
{
Dossier root("/");
root.Add(new Dossier("home/")).Add(new Dossier("david/")).Add(new Fichier("composite.h","text"));

root.Afficher();

}

Comme dans le décorateur, rien de bien dur dans l'implémentation à proprement parler. La seule chose notable est dans la classe de l'objet composite (ici dossier) : la redéfinition des fonctions virtuelles est souvent une boucle qui appelle les fonctions de chaque membre du vecteur.

VI-C - Pas de templates

Comme pour le décorateur, le composite est plus une notion qu'un service clé en main. De ce fait, je pense qu'on peut difficilement fournir une version template du composite.

VII - L'observateur

Définition : L'observateur est une classe dont le rôle est d'être averti quand l'une des classes qu'elle observe change.

VII-A - Pourquoi observer ?

Imaginez que vous deviez réaliser un programme de surveillance de données météorologiques pour un agriculteur qui doit surveiller la pression atmosphérique et la température. Comment réaliser ceci ? On pourrait créer une classe qui interrogerait à intervalles réguliers le thermomètre et le baromètre. Mais ce n'est pas la meilleure des solutions. En effet, on va consommer du CPU pour rien si les valeurs relevées par les instruments ne changent pas.

Il faudrait plutôt faire l'inverse : notre programme doit être averti à chaque changement de valeur et c'est précisément ce que propose l'observateur. À chaque objet observé on attache un observateur qui va être averti via une fonction Update lors d'un changement de valeur. Pour lancer cette notification, une fonction Notify est indispensable dans l'objet observé en plus que celles pour ajouter/enlever un observateur. Ainsi, on voit donc que le pattern a pour diagramme :

Diagramme UML du pattern observateur

VII-B - Première implémentation

Grâce à l'analyse faite ci-dessus, une première implémentation du pattern observateur est tout simplement :

#ifndef OBS_H
#define OBS_H

#include <iostream>
#include <list>
#include <iterator>
#include <algorithm>

typedef int Info;

class Observable;

class Observateur
{
protected:
std::list<Observable*> m_list;
typedef std::list<Observable*>::iterator iterator;
typedef std::list<Observable*>::const_iterator const_iterator;
virtual ~Observateur() = 0;
public:
virtual void Update(const Observable* observable) const ;

void AddObs(Observable* obs);
void DelObs(Observable* obs);
};

class Observable
{
std::list<Observateur*> m_list;

typedef std::list<Observateur*>::iterator iterator;
typedef std::list<Observateur*>::const_iterator const_iterator;

public:
void AddObs( Observateur* obs);
void DelObs(Observateur* obs);

virtual Info Statut(void) const = 0;
virtual ~Observable();
protected:
void Notify(void);

};

class Barometre : public Observable
{
int pression;
public:
void Change(int valeur);

int Statut(void) const;
};

class Thermometre : public Observable
{
int temperature;
public:

void Change(int valeur);

Info Statut(void) const;
};

class MeteoFrance : public Observateur
{

};

#endif

#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>

#include "obs.h"

using namespace std;

void Observateur::Update(const Observable* observable) const
{
//on affiche l'état de la variable
cout<<observable->Statut()<<endl;
}

Observateur::~Observateur()
{
//pour chaque objet observé,
//on lui dit qu'on doit supprimer l'observateur courant
const_iterator ite = m_list.end();

for(iterator itb = m_list.begin(); itb != ite; ++itb)
{
(*itb)->DelObs(this);
}
}

void Observateur::AddObs( Observable* obs)
{
m_list.push_back(obs);
}

void Observateur::DelObs(Observable* obs)
{
//on enlève l'objet observé.
iterator it = std::find(m_list.begin(),m_list.end(),obs);
if(it != m_list.end())
m_list.erase(it);
}

void Observable::AddObs( Observateur* obs)
{
//on ajoute l'observateur à notre liste
m_list.push_back(obs);

//et on lui donne un nouvel objet observé.
obs->AddObs(this);
}

void Observable::DelObs(Observateur* obs)
{
//même chose que dans Observateur::DelObs
iterator it = find(m_list.begin(),m_list.end(),obs);
if(it != m_list.end())
m_list.erase(it);
}

Observable::~Observable()
{
//même chose qu'avec Observateur::~Observateur
iterator itb = m_list.begin();
const_iterator ite = m_list.end();

for(; itb != ite; ++itb)
{
(*itb)->DelObs(this);
}
}

void Observable::Notify(void)
{
//on prévient chaque observateur que l'on change de valeur
iterator itb = m_list.begin();
const_iterator ite = m_list.end();

for(; itb != ite; ++itb)
{
(*itb)->Update(this);
}
}

void Barometre::Change(int valeur)
{
pression = valeur;
Notify();
}

int Barometre::Statut(void) const
{
return pression;
}

void Thermometre::Change(int valeur)
{
temperature = valeur;
Notify();
}

Info Thermometre::Statut(void) const
{
return temperature;
}

int main(void)
{

Barometre barometre;
Thermometre thermometre;
//un faux bloc pour limiter la portée de la station
{
MeteoFrance station;

thermometre.AddObs(&station);
barometre.AddObs(&station);

thermometre.Change(31);
barometre.Change(975);
}

thermometre.Change(45);
return 0;

}

Dans le code en lui-même, je pense qu'il n'y a rien de bien difficile. Par contre, ce qui est un peu plus dur à comprendre, c'est le pattern en lui-même. À chaque changement de valeur dans une classe observée, on appelle la méthode Notify qui va signaler à tous les observateurs de l'objet "J'ai changé de valeur ! Demande-moi ma nouvelle valeur". Mais pour l'observateur qui est ce "J'" ? Comment peut-il le savoir ? C'est pourquoi il y a le passage d'un pointeur vers l'objet observé lors de l'appel à Update.

Ici info est un typedef. Mais dans un cas réel on pourrait plutôt passer par des classes du type InfoBarometre ou InfoThermometre dérivant toutes d'une interface InfoBase avec un opérateur appelant une fonction Afficher polymorphique.

VII-C - Remarque sur ce pattern

VII-C-1 - Tirer/Pousser

Il existe deux manières de récupérer l'information dans la méthode Update. Une première consiste à "tirer" l'information de l'objet en appelant une méthode qui va nous donner l'information. La deuxième consiste au contraire à "pousser". C'est l'objet observé qui va pousser l'information jusqu'à l'observateur. Ici, j'ai donc adopté la méthode de tirer qui est plus simple et permet plus de modularité.

VIII - La stratégie

Stratégie : pattern permettant de changer d'algorithme utilisé de façon dynamique.

VIII-A - Pourquoi adopter la stratégie

Imaginez : vous être en train de coder une IA. Celle-ci doit effectuer différentes actions selon la situation dans laquelle elle se trouve. Comment coder ceci ? On pourrait le faire à coup de if/else mais cela serait vite une horreur. La solution la plus avantageuse est alors de passer par le pattern stratégie qui va encapsuler chaque version de l'algorithme dans une classe. Ensuite il suffira de moduler les classes appelées par le client pour changer l'algorithme utilisé de manière transparente. Avec cette petite analyse, on en déduit que le diagramme UML de ce pattern est le suivant :

Diagramme UML du pattern stratégie

VIII-B - Première implémentation

En reprenant l'idée d'une IA abordée dans l'introduction, un 1er exemple de ce pattern peut être

#ifndef STRATEGIE_H
#define STRATEGIE_H

class Strategie;

class IA
{
int m_puissance;
Strategie* m_strategie;

void  SetStrategie(Strategie* strategie);
public:
IA();
~IA();

void ChangePuissance(int i);
void  Jouer();
};

class Strategie
{
public:
void Jouer();

virtual ~Strategie() = 0;
protected:
virtual void Analyser() = 0;
virtual void Appliquer() = 0;

};

class Offensive: public Strategie
{
void Analyser();
void Appliquer();

};

class Defensive: public Strategie
{
void Analyser();
void Appliquer();
};

#endif

Et dans le fichier stratégie.cpp

#include <iostream>
#include "strategie.h"
using namespace std;

IA::IA():m_strategie(new Offensive)
{
m_puissance = 100;
}

void IA::Jouer()
{
if(m_puissance < 50)
SetStrategie(new Defensive);

m_strategie->Jouer();
}

void IA::SetStrategie(Strategie* strategie)
{
if(strategie != 0)
{
delete m_strategie;
m_strategie = strategie;
}
}

void IA::ChangePuissance(int i)
{
m_puissance += i;
}

IA::~IA()
{
delete m_strategie;
}

void Strategie::Jouer()
{
Analyser();
Appliquer();
cout<<"======"<<endl;
}

Strategie::~Strategie()
{

}

void Offensive::Analyser()
{
cout<<"Envoyer des véhicules de reconnaisance"<<endl;
}

void Offensive::Appliquer()
{
cout<<"Envoyer troupes où il y a des ennemis"<<endl;
}

void Defensive::Analyser()
{
cout<<"Se baser sur le radar"<<endl;
}

void Defensive::Appliquer()
{
cout<<"Construire defense ou il y a des ennemis"<<endl;
}

int main(void)
{
IA ia1;

for(int i=0; i<10; i++)
{
ia1.ChangePuissance(-10);
ia1.Jouer();
}
return 0;
}

On voit donc ici qu'en fonction de la puissance dont elle dispose l'IA va adopter une stratégie différente sans que cela soit visible pour l'utilisateur. Sinon, il n'y a rien de difficile dans le code.

VIII-C - Remarques sur la stratégie

VIII-C-1 - Parallèle avec les politiques

Ceux qui ont déjà lu l'excellent article d'Alp Mestan sur les politiques ont sans doute fait le rapport avec le pattern présenté ici. En effet, les classes de politiques sont la représentation du pattern stratégie via les templates. Sauf que celle-ci utilise le polymorphisme statique au lieu d'utiliser le polymorphisme dynamique. Je vous propose donc de vous référer à son article pour un exemple d'utilisation.

IX - Conclusion

IX-A - Que retenir de mon article ?

Cette question peut paraître étrange, mais elle a tout son sens. En effet, durant tout l'article je vous ai présenté des codes illustrant des concepts. Et ce sont bien sûr ces concepts qui sont à retenir. Les codes (pas toujours optimisés ou conçus au mieux) sont là pour présenter de manière simple et concrète le concept. Sachez que les patterns que j'ai présentés ici sont ceux que je considère comme les plus utiles/courants. Néanmoins, sachez quand même qu'il existe un bon nombre d'autres designs patterns.

IX-B - Références

Voici les références que j'ai utilisées. J'en profite aussi pour glisser quelques articles pour approfondir certains patterns. De façon générale :

- Wikipédia version anglaise et française

- L'article de Pierre Caboche (ici)

Et de façon plus précise en tant que source ou complément :

- Fabrique

- Exemple de fabrique via les fonctions statiques

- Singleton

- Le problème du DLCP par Scott Meyers and Andrei Alexandrescu

- Observateur

- Une amélioration du pattern via boost::function

- Une autre approche du pattern

IX-C - Remerciements

Je souhaite remercier en particulier Alp qui ma lancé dans cette aventure. Luc Hermitte, millie et Miles pour leurs conseils et soutient ainsi qu'à RideKick, bassim et la team du Nyug pour la relecture.