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

- 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. Clonage

- II-1. Mise en situation et première solution

- II-2. Améliorations

- II-3. Application

- III. Le singleton

- 1. Quand a-t-on besoin du singleton ?

- 2. Exemple simpliste d'implémentation du Singleton

- 3. Implémentation templatisée

- 4. Remarques sur le singleton

- 4-a. Le singleton non thread-safe

- 4-a-1. Première solution envisageable

- 4-a-2. Problème lié à cette solution

- 4-a-3. Problème lié au double check

- 4-b. Le singleton est une variable globale

- IV. La fabrique

- IV-1. Un besoin de fabrique

- IV-2. Définition générique de la fabrique

- IV-3. Définition des classes de politique

- IV-4. Utilisation

- IV-5. Améliorations

- V. Le décorateur

- V-1. Pourquoi le décorateur

- V-2. Les bases nécessaires avant de créer le pattern

- V-3. Le pattern en lui même

- V-5. Remarques

- VI. Le composite

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

- VI-2. Première implémentation

- VII. L'observateur

- VII-1. Pourquoi observer ?

- VII-2. Première implémentation

- VII-3. Remarque sur ce pattern

- a. Tirer/Pousser

- VIII. La stratégie

- VIII-1. Pourquoi adopter la stratégie

- VIII-2. Première implémentation

- VIII-3. Remarques sur la stratégie

- VIII-3-a. Parallèle avec les politiques

- IX. Conclusion

- IX-1. Que retenir de mon article ?

- IX-2. Références

- IX-3. 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 - Clonage

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

II-1 - Mise en situation et première solution

La sémantique des classes en C++ se décompose principalement selon les classes de valeur et les classes d'entité. Les premières sont copiables, assignables mais ne font pas partie d'une hiérarchie d'héritage. A l'inverse, les secondes sont souvent utilisées dans un but de polymorphisme dynamique et ne sont pas copiable. Néanmoins, il y'a des cas où on a besoin de copier ces objets.

Pour rendre le concept plus clair, supposons une classe SGBDConnexion dont hérite publiquement les classes MySQLConnexion et AccesConnexion. A cause du LSP, partout où on attend une objet de type SGBDConnexion, on pourra passer un objet de type MySQLConnexion ou AccesConnexion. Comment peut on faire pour copier un objet qui a été casté en objet du type SGBDConnexion dans ces conditions ?

class SGBDConnexion
{
public:
virtual ~SGBDConnexion() =0;
};

SGBDConnexion::~SGBDConnexion(){}

class MySQLConnexion: public SGBDConnexion
{
MySQLDriver m_driver;
public:
virtual ~MySQLConnexion(){}
};
class AccesConnexion: public SGBDConnexion
{
AccesDriver m_driver;
public:
virtual ~AccesConnexion(){}
};
void foo(SGBDConnexion& a)
{
//comment peut on copier l'objet a ?
}

La solution semble se trouver dans les fonctions virtuelles. En effet, si A dispose d'une fonction virtuelle clone qui copie l'objet courant, alors on pourra dans les classes B et C redéfinir la fonction pour qu'elle copie un objet de type B ou C.

On arrive donc au schéma UML suivant:

Diagramme UML du pattern prototype

//on complétant les classes du dessus, on a:
class SGBDConnexion
{
public:
SGBDConnexion* clone () const =0;
};
class MySQLConnexion: public SGBDConnexion
{
public:
MySQLConnexion* clone () const {return new MySQLConnexion(*this);}
};
class AccesConnexion: public SGBDConnexion
{
public:
AccesConnexion* clone () const {return new AccesConnexion(*this);}
};

Que se passe-t-il ?

Dans un premier temps, on définit une fonction clone virtuelle pure qui renvoie un pointeur de type SGBDConnexion. Pourquoi un pointeur et non une référence ? Pour ne pas retourner une référence sur un objet temporaire, référence qui ne serait plus valide une fois l'exécution de la fonction finie.

Ensuite, dans les classes dérivées, le type de retour de la fonction change. Le compilateur devrait en théorie m'insulter de tous les nom. Mais il n'en n'est rien. En effet, on utilise ici les retours covariants. 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.

II-2 - Améliorations

Ce code présente deux problèmes majeurs: il renvoie des pointeurs bruts (une calamité à gérer) et présente un certain nombre de redondance dans les définitions des fonctions clone.

La première amélioration que l'on peut faire est d'utiliser des boost::shared_ptr pour éviter d'avoir des pointeurs bruts. Mais alors on ne peut plus utiliser les retours covariants. En effet, il n'y a aucun lien d'héritage entre shared_ptr<SGBDConnexion> et shared_ptr<MySQLConnexion>. On va donc renvoyer partout des shared_ptr<SGBDConnexion>.

class SGBDConnexion
{

public:
virtual ~SGBDConnexion() =0;
virtual const shared_ptr<SGBDConnexion> clone() const =0;

};

SGBDConnexion::~SGBDConnexion(){}

class MySQLConnexion: public SGBDConnexion
{
public:
virtual ~MySQLConnexion(){}
virtual const shared_ptr<SGBDConnexion> clone() const
{
return shared_ptr<SGBDConnexion>( new MySQLConnexion(*this));
}
};
class AccesConnexion: public SGBDConnexion
{
public:
virtual ~AccesConnexion(){}
virtual const shared_ptr<SGBDConnexion> clone() const
{
return  shared_ptr<SGBDConnexion>(new AccesConnexion(*this));
}
};

void foo(SGBDConnexion& a)
{
shared_ptr<SGBDConnexion> ptr = a.clone();
}

On se retrouve donc avec un code fonctionnel mais qui dispose de certaines redondances au niveau du code de clone: seul le type d'objet diffère. On peut donc factoriser ce code dans une fonction template. Reste le problème de l'endroit où l'on va définir cette fonction. On peut soit la définir en tant que fonction libre, soit en tant que fonction membre statique. L'avantage de cette dernière option est qu'on peut limiter la visibilité de la fonction aux classes dérivées.

class SGBDConnexion
{
//. comme avant
protected:
template <class T> static shared_ptr<SGBDConnexion> do_clone(const T* obj)
{
return shared_ptr<SGBDConnexion>( new T(*obj));
}
};

class MySQLConnexion: public SGBDConnexion
{
public:
virtual ~MySQLConnexion(){}
virtual const shared_ptr<SGBDConnexion> clone() const
{
return  do_clone(this);
}
};

class AccesConnexion: public SGBDConnexion
{
public:
virtual ~AccesConnexion(){}
virtual const shared_ptr<SGBDConnexion> clone() const
{
return  do_clone(this);
}
};

Grâce à la déduction automatique des arguments templates, le code réel de clone est identique en apparence. Mais néanmoins, il existe une grande différence, tous les pointeurs this ont un type différent, c'est pourquoi on ne peut pas factoriser le code de la fonction membre clone dans la classe SGBDConnection.

II-3 - Application

Cette technique du clonage permet d'améliorer les performances dans un certain nombres de cas. En effet, certains objets (comme un connexion à une base de données) sont lourd à construire: il faut se connecter à la base puis s'identifier, procédure plutôt coûteuse. Si on a besoin de travailler sur une autre table d'une même base, il suffit de cloner la connexion existante et de modifier la table sur laquelle on travaille. La copie pouvant se faire quasiment bit-par-bit, le compilateur est en mesure d'effectuer ceci très rapidement. A la fin de l'opération de clonage, on se retrouve alors avec 2 connexions opérationnelles mais dont le temps de construction aura été plus rapide que si on avait reconstruit toute la connexion depuis zéro. On retrouve la même idée avec la fonction fork sur les systèmes Un*x.

III - Le singleton

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

1 - 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 jeux 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

2 - 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 ny 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 appellée ? Personne ne va utiliser SoundManager. Néamoins, un objet est quand même crée ! 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.

3 - 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'appeller 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.

4 - Remarques sur le singleton

4-a - 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();
}

4-a-1 - 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;
}

4-a-2 - 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 tous 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.

4-a-3 - 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.

4-b - 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-1 - 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 fatiguant. 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-2 - Définition générique de la fabrique

Nous allons d'abord écrire la manière d'utiliser la classe (son interface) avant de penser à la manière de la définir. On souhaite ,avec une même classe, pouvoir gérer plusieurs hiérarchies différentes (avec tous les types de clés possibles ) et posséder plusieurs méthodes d'instanciation différentes. Le code d'utilisation va donc ressembler à quelque chose comme cela:

Fabrique<Key,Base,Methode> c;
c.Register(key1,object1);
c.Register(key2,object2);

Avant de continuer définissons notre hiérarchie qui sera la classique hiérarchie de figures géométriques.

struct Shape {virtual void draw()=0;};
struct Square: Shape
{
void draw(){std::cout<<"Square"<<std::endl;}
};
struct Circle: Shape {
void draw(){std::cout<<"Circle"<<std::endl;}
};
struct Triangle : Shape{
void draw(){std::cout<<"Triangle"<<std::endl;}
};

Notre classe fabrique doit pouvoir gérer n'importe quel type de clé, de hiérarchie ou encore de méthode de création. La seule contrainte sur cette dernière sera la présence d'une fonction membre create. L'utilisation des templates et des classes de politiques est donc toute adaptée.

template
<
typename Key,typename Object,
template <class,class> class PolicieCreation
>
struct Fabrique
{
private:
PolicieCreation<Key,Object> creator;
};

Nous définissons ainsi une classe template Fabrique dont les deux premiers arguments templates représentent la clé et la hiérarchie. Le dernier est une classe template prenant elle aussi 2 arguments et qui représente la méthode de création des objets. Notez qu'aucun nom n'a été donné aux arguments templates du dernier paramètres car ces derniers ne nous intéressent pas. A l'intérieur de la classe nous déclarons un objet du type PolicieCreation en lui fournissant comme paramètre les types Key et Object. Définissons maintenant les fonctions register et create.

//Way représente le moyen de créer un objet (ici, c'est un boost::function)
void Register(Key key,typename PolicieCreation<Key,Object>::Way obj){creator.register(key,obj);}

//ici Arg est le type d'argument que la fonction create de la classe de politique va demander
boost::shared_ptr<Object> Create(typename PolicieCreation<Key,Object>::Arg key){return creator.create(key);}

Il faut, avant toute chose, comprendre que via l'utilisation des classes de politique, nous laissons le choix total à l'utilisateur sur le fonctionnement des classes. Seuls les prototypes des fonctions membres sont imposés. A ce titre, c'est à la classe de politique de politique de spécifier via des typedef publiques le type d'objet qu'elle attend pour instancier des objets (Way, en pratique un boost::function) et le type d'argument lors d'une requête pour créer un objet (Arg). Le contenu des fonctions est extrêmement simple, il n'y a que des appels aux fonctions respectives de la classe de politique.

IV-3 - Définition des classes de politique

Pour expliciter la nature modulaire de la fabrique, nous allons définir deux classes de création. La première récupérera directement ses arguments en tant que paramètre de la fonction. La seconde récupérera un flux dont elle extraira ses arguments.

De part l'utilisation qu'on à fait dans la section d'avant de nos classes de création, celles ci doivent toutes ressembler à cela :

template <typename Key,typename Object> struct Creator
{
typedef boost::function < boost::shared_ptr<Object> () > Way;

void Register(Key key,Way obj) ;
shared_ptr<Object> Create (Arg arg) const;

private:
std::map<Key,Way> m_map;
};

Il ne reste alors plus qu'à préciser Arg et le contenu des diverses fonctions.

Dans le cas d'une politique où la récupération des arguments va se faire directement par les paramètre de la fonction, il y à coïncidence entre Arg et Key (à l'ajout près d'un qualificatif de référence constante). Le corps des fonctions membres sera extrêmement simple. Register enregistrera le couple clé/boost::function tandis que Create fera un simple appel à ces fonctions.

template <typename Key,typename Object> struct SimpleCreator
{
typedef boost::function < boost::shared_ptr<Object> () > Way;
typedef const Key& Arg;

void Register(Key key,Way obj)
{
// si la clé n'est pas présente on l'enregistre.
if(m_map.find(key)==m_map.end()) { m_map[key]=obj; }
}

shared_ptr<Object> Create (Arg key) const
{
shared_ptr<Object> tmp;

typename std::map<Key,Way>::const_iterator it=m_map.find(key);
if(it!=m_map.end()) { tmp=((*it).second)();}

return tmp;
}
private:
std::map<Key,Way> m_map;
};

Dans le cas où la récupération d'argument se fera par la lecture d'un flux, Arg sera une référence sur un std::istream et le corps de Create sera modifié en conséquence : il faudra d'abord lire le flux pour en récupérer le contenu sous forme d'une chaîne de caractères avec std::getline, puis convertir cette châine en Key.

template <typename Key,typename Object> struct StreamCreator
{
typedef boost::function < boost::shared_ptr<Object> () > Way;
typedef std::istream& Arg;

void Register(Key key,Way obj)
{
if(m_map.find(key)==m_map.end()) { m_map[key]=obj;}
}

shared_ptr<Object> Create (Arg key) const
{
shared_ptr<Object> tmp;
std::string s; std::getline(key,s); // pas de gestion d'erreur

typename std::map<Key,Way>::const_iterator it=m_map.find(convert<Key>(s));
if(it!=m_map.end()) { tmp=((*it).second)();}

return tmp;
}

private:
std::map<Key,Way> m_map;
};

La réalisation de la conversion se fait par une fonction template libre nommée convert qui va convertir une chaîne de caractère en Key. Dans le cas où Key est elle même une chaîne de caractère, la fonction ne doit rien faire. On arrive alors au code suivant:

template <class Output> Output convert(const std::string& s)
{
std::istringstream iss( s );
Output o; iss>>o;
return o;
}

template <> std::string convert(const std::string& s)
{
return s;
}

On peut se demander pourquoi convert existe en tant que fonction libre et non en tant que fonction membre de StreamCreator. La réponse est assez simple et provient d'une limitation du langage : on ne peut pas réaliser une spécialisation d'une fonction membre template d'une classe template sans avoir avant spécialisé la classe en elle même. Dès lors, la nécessité d'une fonction libre s'impose.

IV-4 - Utilisation

L'utilisation de notre classe template va être assez simple compte tenu du travail fait avant. On définit en premier lieu diverses fonctions et structures pour tester notre fabrique:

namespace
{
shared_ptr<Shape> CreateCircle(){return shared_ptr<Shape>(new Circle());}
shared_ptr<Shape> CreateSquare(){return  shared_ptr<Shape>(new Square());}
shared_ptr<Shape> CreateTriangle(){return shared_ptr<Shape>(new Triangle());}
}

struct Test
{
boost::shared_ptr<Shape> triangle() const {
return boost::shared_ptr<Shape>(new Triangle);
}
};

Puis on définit notre fabrique tel qu'elle a été présentée au début avec les différents choix présentés.

int main(int argc, char const *argv[])
{

Foo f;
Fabrique<std::string,Shape,SimpleCreator> c;
Fabrique<std::string,Shape,StreamCreator> c1;

Fabrique<int,Shape,SimpleCreator> cc;

c.Register("Square",&CreateSquare);
c.Register("Circle",&CreateCircle);
c.Register("Triangle",boost::bind(&Test::triangle,f));

c1.Register("Square",&CreateSquare);
c1.Register("Circle",&CreateCircle);
c1.Register("Triangle",boost::bind(&Test::triangle,f));

cc.Register(0,&CreateSquare);
cc.Register(1,&CreateCircle);
cc.Register(2,boost::bind(&Test::triangle,f));

shared_ptr<Shape> p= c.Create("Circle");
p->draw();

shared_ptr<Shape> p2= c1.Create(std::cin);
p2->draw();

shared_ptr<Shape> p3= cc.Create(2);
p3->draw();

return 0;
}

IV-5 - Améliorations

Cette fabrique est loin d'être parfaite, vous pouvez en tant qu'exercice la compléter pour ajouter un support des arguments lors de la construction des objets (rendu plus facile avec les futurs variadic templates de C++0X ).

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 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.

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).

VI - Le composite

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

VI-1 - 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-2 - 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 :

#include <iostream>
#include <string>
#include <vector>
#include <boost/shared_ptr.hpp>

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

protected:
std::string m_nom;

public:
virtual int GetTaille() const { return m_taille; }

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

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

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

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

Fichier(const std::string& nom,const std::string& type):
FichierAbstrait(nom),m_type(type)
{}
};

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

public:

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

Dossier& Add(Dossier* file)
{
m_listfichier.push_back(boost::shared_ptr<Dossier>(file));
return (*file);
}

void Add(Fichier* file)
{
m_listfichier.push_back(boost::shared_ptr<Fichier>(file));
}

Dossier(const std::string& path):FichierAbstrait(path)
{}
};

Et dans le fichier composite.cpp

#include "composite.h"

const int FichierAbstrait::m_taille=1;
FichierAbstrait::~FichierAbstrait() {}

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

std::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;

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

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

return somtaille;
}

int main(void)
{
Dossier root("/");
root.Add(new Dossier("home/")).Add(new Dossier("david/")).Add(new Fichier("composite.h","text"));
root.Afficher();
}

Quelques petits points à souligner.

Le premier d'entre eux est sur le destructeur de FichierAbstrait. Comme son nom l'indique, cette classe est abstraite, elle doit donc posséder au moins une fonction virtuelle pure. Or dans notre cas, aucune ne semble convenir. Dans ce cas, il faut se rabattre sur le destructeur et le rendre virtuel. Néanmoins, il se pose un problème. Lors de la destruction des classes dérivés, les objets vont forcément appeler le constructeur de la classe parente, on ne peut donc pas laisser ce destructeur sans définition et c'est pourquoi on le définit dans le C++. La plupart d'entre vous doivent se dire que cela va à l'encontre de ce qu'ils savent des fonctions virtuelles. Il faut bien noter qu'une fonction virtuelle peut être redéfinie par une classe dérivée alors qu'une fonction virtuelle pure doit l'être si on veut que la classe soit instanciable. Néanmoins, le C++ ne nous empêche de donner une définition à une fonction virtuelle pure, elle est juste facultative.

L'utilisation des boost::shared_ptr (comme dans le cas du décorateur) permet d'allouer dynamiquement les dossiers et fichiers tout en étant sûr qu'il seront copiés et détruits convenablement. Si on souhaite utiliser des objets alloués sur la pile, il ne faut pas utiliser des shared_ptr.

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-1 - 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-2 - 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-3 - Remarque sur ce pattern

a - 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-1 - 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-2 - 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 ennemies"<<endl;
}

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

void Defensive::Appliquer()
{
cout<<"Construire defense ou il ya des ennemies"<<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-3 - Remarques sur la stratégie

VIII-3-a - 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-1 - 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-2 - Références

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

- Wikipedia 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-3 - 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.