Les Design Patterns - Partie 1/4
Un article de TheClems.
Attention
Cet article est en attente du choix d'une licence par son auteur. Elle n'est pas soumise à la licence par défaut du GCN.
Cela signifie entre autre chose que si vous n'êtes pas l'auteur de cet article, vous n'avez probablement pas le droit de le modifier.
Sommaire |
[modifier] Prérequis
- Bonne connaissance des concepts de POO (sans, c'est même pas la peine) - Notions de C++ ;
- Quelques exemples utilise la STL, mais ce n'est pas obligatoire pour la compréhension générale du document ;
- Savoir à peu près lire un schéma UML de classe ou alors avoir assez d'instinct pour comprendre la signification de toutes ces pitites flèches !
[modifier] But du document
- Montrer les avantages de l'utilisation des Design Patterns (DP) ;
- Illustrer ces DP par des exemples ayant un lien avec la programmation de jeux vidéo ;
- Donner envie de connaître plein de DP funky.
[modifier] Introduction
[modifier] Qu'est-ce qu'un design pattern ?
Les design patterns (DP) sont tout simplement des architectures de classes permettant d'apporter une solution à des problèmes fréquemment rencontrés lors des phases d'analyse et de conception d'applications. Ces solutions sont facilement adaptables (donc réutilisables), elles sont utilisables sans aucun risque dans la grande majorité des langages de programmation orientés objet.
[modifier] Pourquoi les design patterns ?
- Pour ne pas avoir à réinventer la roue ;
- Les DP sont fiables ;
- Les DP ont une architecture facilement compréhensible et identifiables pour un programmeur (améliore la communication).
[modifier] Comment utiliser les design patterns ?
Les DP sont des organisations de classes, qu'il vous faudra adapter à votre problème, et là est la seule véritable difficulté. En effet, ce document présente les DP et leur fonctionnement ainsi que pour chaque, un exemple d'utilisation, tout ceci pour vous faire réfléchir aux questions principales :
- Quand utiliser un DP ? (autrement dit : identifier les problèmes) ;
- Quel DP utiliser ? (cerner les problèmes identifiés) ;
- Comment le mettre en oeuvre ? (résoudre le problème).
Les DP ne sont donc pas vraiment une solution miracle pour les problèmes, mais ce sont plutôt des méthodes de résolutions. Un DP c'est comme une formule mathématique, c'est la solution mais encore faut-il l'appliquer au bon moment avec les bonnes variables.
[modifier] Un (tout) ch'tit peu d'histoire…
Les auteurs des principaux design patterns (il y en a 23), plus connus sous le nom de Gang Of Four, sont Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides. Les DP qu'ils ont conçus sont considérés comme la base de tout les autres (car il y en a moults autres) et sont organisés en 3 catégories (creationnal, structural, behavioral). Leur livre sur les DP reste la référence.
Ce document présente seulement les DP du GOF (gang of four) a raison de deux par catégorie (d'autres documents suivront). Il est assez facile de trouver d'autres design patterns sur internet (faites chauffer Google).
[modifier] Design Patterns Creationnal
[modifier] Définition
Les patterns de cette catégorie concernent les problèmes inhérents à la création d'objets.
[modifier] Singleton
[modifier] Qu'est ce qu'un singleton ?
Un singleton est une classe qui ne produit qu'une seule instance, et cette dernière est accessible de partout. Pour cela, il faut que la construction d'une nouvelle instance grâce à l'opérateur new soit interdite. La solution est de rendre le constructeur de la classe privé. Mais maintenant comment récupérer une instance de la classe ?
[modifier] Implémentation
Regardons le Schéma UML suivant, même si il n'est pas très parlant, je l'avoue :
Puis ce Code d'exemple :
class Singleton { private : Singleton(); public : static Singleton * getSingleton(); }; Singleton::Singleton() { } Singleton * Singleton::getSingleton() { static Singleton instanceUnique; return & instanceUnique; } int main() { Singleton * s1 = Singleton::getSingleton(); Singleton * s2 = Singleton::getSingleton(); return 1; }
Dans ce code, s1 et s2 font tout les deux référence à l'unique instance de la classe Singleton. La méthode statique getSingleton est celle qui prend en charge la création du singleton, qui est une variable statique (donc persistante). Le singleton n'est donc construit qu'une seule fois lorsque getSingleton est appelée pour la première fois. La destruction du singleton est aussi automatique (à la fin de programme les objets persistants sont détruits).
[modifier] Petite remarques sur le singleton
Les paramètres aux constructeurs :
Dans l'exemple le singleton n'en a pas, mais dans certains cas (et dans certains cas seulement) le constructeur peut avoir besoin de paramètres n'étant pas constants d'une exécution sur l'autre. Dans ce cas la, il y a de nombreuses solutions possibles, en voici quelques unes :
- Stocker ces paramètres sous forme de membres statiques de la classe et créer une méthode pour les modifier. Mais il faut pouvoir être sur que ces paramètres seront initialisé avant le premier appel à la méthode getSingleton.
- Passer ces valeurs en paramètres a la fonction getSingleton. Mais d'un point de vue utilisation, c'est pas le top.
Imaginons le code suivant :
Singleton * s = Singleton::getSingleton(12); ... autre part dans le code... Singleton * s2 = Singleton::getSingleton(23);
Eh oui, on croit utiliser avec s2 un singleton construit avec la valeur 23 mais en fait il a déjà été construit avec la valeur 12…
Les paramètres par défaut permettent de masquer ce piti problème (mais c'est loin d'être élégant et bien conçu). Donc cette solution est à proscrire à moins d'avoir une classe chargée de la création des singletons.
Une dernière solution et c'est fini.
Pour cette dernière solution, on va changer un peu l'implémentation du singleton en rajoutant une fonction destinée à la création.
class Singleton { private: int attribut; Singleton(int a); static Singleton * instanceUnique; public: static void createSingleton(int a); static Singleton * getSingleton(); }; // implementation Singleton * Singleton::instanceUnique = NULL; Singleton::Singleton(int a) { attribut = a; } void Singleton::createSingleton(int a) { if (!instanceUnique) { instanceUnique = new Singleton(a); } } Singleton * Singleton::getSingleton() { return instanceUnique; } // utilisation Singleton::createSingleton(12); Singleton * s = Singleton::getSingleton();
Attention au piège de cette implémentation : Il faut être sur que le singleton a été crée avant de le récupérer. La destruction n'est plus automatique, il faut prévoir une méthode statique pour détruire l'instance.
Le singleton est connu pour être utilisé très (voire trop) souvent.. C'est vrai qu'il offre des avantages indéniables lorsqu'on veut par exemple avoir une seule instance de classes représentant la gestion du son, ou le moteur d'affichage et bien d'autres encore.
[modifier] Un exemple concret d'utilisation du singleton
Supposons que pour gérer les textures on ait une classe CTextureManager qui permet de gérer des files de priorité de textures pour utiliser au mieux la mémoire vidéo. Cette classe doit avoir une seule instance qui sera par exemple utilisée par divers modules (ajouter un fichier textures, précharger des textures, utiliser une texture, etc...). Cette classe est typiquement implémentable en Singleton (en plus la libération des textures en fin de programme seras automatiquement effectué avec un bon coup de destructeur, gain de temps non négligeable !)
[modifier] Prototype
[modifier] Qu'est ce qu'un prototype ?
Le prototype est le pattern qui va vous permettre de créer des copies exactes d'objets sans même connaître leurs types. C'est donc une extension du constructeur par copie pour pouvoir travailler avec du polymorphisme.
Dans le schéma UML suivant, on voit apparaître une classe en plus : "Client". Cette classe est celle qui nous permet l'accès à l'ensemble des prototypes.
Code d'exemple : ce code montre le concept du prototype avec des objet Couleur dans différents codages (hsv, rgb, rgba).
class CouleurAbstrait { public: CouleurAbstrait(); virtual ~CouleurAbstrait() {} virtual void display() = 0; virtual CouleurAbstrait * clone() = 0; }; class CouleurRGB : public CouleurAbstrait { protected : int R; int G; int B; public : CouleurRGB(int r, int g, int b); virtual ~CouleurRGB() {} virtual void display(); virtual CouleurAbstrait * clone(); }; class CouleurRGBA : public CouleurRGB { protected : int A; public : CouleurRGBA(int r, int g, int b, int a); virtual ~CouleurRGBA() {} virtual void display(); virtual CouleurAbstrait * clone(); }; class CouleurHLS : public CouleurAbstrait { protected : int H; int L; int S; public : CouleurHLS(int h, int l, int s); virtual ~CouleurHLS() {} virtual void display(); virtual CouleurAbstrait * clone(); }; class BoiteDePeinture { protected : typedef std::map<std::string,CouleurAbstrait *> mapCouleur; mapCouleur couleurs; public : BoiteDePeinture(); ~BoiteDePeinture(); CouleurAbstrait * getCouleur(std::string nom); }; // Implementation CouleurAbstrait::CouleurAbstrait() {} CouleurRGB::CouleurRGB(int r, int g, int b) :CouleurAbstrait(), R(r), G(g), B(b) {} void CouleurRGB::display() { std::cout << "RGB : " << R << ' ' << G << ' ' << B << std::endl; } CouleurAbstrait * CouleurRGB::clone() { return new CouleurRGB(*this); } CouleurRGBA::CouleurRGBA(int r, int g, int b, int a) :CouleurRGB(r, g, b), A(a) {} void CouleurRGBA::display() { std::cout << "RGBA : " << R << ' ' << G << ' ' << B << ' ' << A << std::endl; } CouleurAbstrait * CouleurRGBA::clone() { return new CouleurRGBA(*this); } CouleurHLS::CouleurHLS(int h, int l, int s) :CouleurAbstrait(), H(h), L(l), S(s) {} void CouleurHLS::display() { std::cout << "HLS : " << H << ' ' << L << ' ' << S << std::endl; } CouleurAbstrait * CouleurHLS::clone() { return new CouleurHLS(*this); } BoiteDePeinture::BoiteDePeinture():couleurs() { couleurs["rouge"] = new CouleurRGB(255,0,0); couleurs["vert"] = new CouleurRGB(0,255,0); couleurs["rougeHLS"] = new CouleurHLS(255,128,255); couleurs["vertTranslucide"] = new CouleurRGBA(0,255,0,128); } CouleurAbstrait * BoiteDePeinture::getCouleur(std::string nom) { return couleurs[nom]-> clone(); } BoiteDePeinture::~BoiteDePeinture() { for (mapCouleur::iterator i = couleurs.begin(); i != couleurs.end(); i++) { delete i- > second; } } int main(void) { BoiteDePeinture boite; CouleurAbstrait * c1 = boite.getCouleur("rouge"); CouleurAbstrait * c2 = boite.getCouleur("rougeHLS"); CouleurAbstrait * c3 = boite.getCouleur("vert"); CouleurAbstrait * c4 = boite.getCouleur("vertTranslucide"); c1-> display(); c2-> display(); c3-> display(); c4-> display(); delete c1; delete c2; delete c3; delete c4; return 0; }
Résultat de l'exécution :
RGB : 255 0 0 HLS : 255 128 255 RGB : 0 255 0 RGBA : 0 255 0 128
[modifier] Petite remarques sur le prototype :
Le prototype s'utilise seulement lorsqu'on a besoin d'obtenir des copies exactes d'objet sans connaître leurs types, sinon, il va de soit qu'un simple constructeur par copie suffit.
Veillez à correctement définir le constructeur par copie des Prototypes, dans l'exemple ci dessus, on utilise le constructeur de copie par défaut des classes pour la simple raison qu'on n'utilise aucunes données membres dynamiques. Dans le cas contraire il vaut mieux définir son propre constructeur de copie pour pouvoir copier les données dynamiques (et non recopier les pointeurs à la sauvage).
Dans certains cas, on préfèrera que les données dynamiques ne soit pas recopiées. Par exemple si votre prototype contient un pointeur vers un Modele3D (objet généralement lourd en terme d'occupation mémoire), on peut recopier seulement les pointeurs mais il faudra implémenter une méthode de nettoyage DISTINCTE du destructeur (chargée de la destruction du modèle 3D) appelée avant la destruction du prototype même (en s'assurant bien sur qu'il ne reste aucun clone du prototype susceptibles d'utiliser le modèle 3D).
[modifier] Un exemple concret d'utilisation du DP Prototype
Imaginons un jeu dans lequel le personnage visite des donjons très très méchant avec plein de saloperies dedans. En tant que game designer sans contraintes et tout plein d'idées géniales, vous avez conçu un super algorithme qui, à partir des données du joueur (expérience, vie max, etc...) calcule les caractéristiques des différents types de monstres peuplant le donjon. Supposons encore un peu plus (oui je cherche des raisons EVIDENTES pour implémenter le Prototype), l'algorithme est codé de manière très subtile, mais gros problème, il est très lent... Mon dieu, mais que faire??? Garder dans une classe spéciale toutes les caractéristiques de chaque Monstres (ouh, quelle horreur) ?
Non non et non, on va utiliser un DP pour s'en sortir en ayant la classe (et en plus on pourras briller en société avec de magnifiques tirades : "Oui, moi, dans MON jeu, j'ai utilisé un design pattern du GOF...").
Première chose à faire, identifier les classes…
Le prototype Abstrait, cela va de soi, c'est une classe Monstre dont tout les monstres dérivent. On a donc comme prototypes Concrets des charmantes classes aux doux noms d'oiseaux du style : MonstreA2TetesQuiPeteDuFeu, MinotaureEnRut, etc... Il ne manque plus que la classe Client, qui je le rappelle, est une classe qui stocke tout les prototypes. Une classe "Bestiaire" dont les instances sont membres de classes "Donjon" fera l'affaire.
On peut faire une petite remarque maintenant : il peut s'avérer fort sympathique de ne jamais détruire le bestiaire d'un donjon. De cette manière, si les monstres sont trop fort, le joueur peut sortir du donjon, s'améliorer un peu et revenir leur casser la gueule sans avoir une zoli surprise du style "Tiens, je suis plus fort qu'avant, mais le Monstre A Deux Têtes qui pètent du feu aussi...".
Bon c'est bien joli tout ça mais un ptit peu de code pour illustrer le concept ne feras pas de mal.
// Interface class Bestiaire { private: std::map<typeCleCreature,Creature *> creatures: // A vous de voir pour le type de la cle, une chaine, un enum, etc... public: Bestiaire(); virtual ~Bestiaire(); Creature * getNewCreature(typeCleCreature cle); void addNewCreature(Creature * creature); }; class Creature { protected : // plein d'attributs folkloriques // pv, attaque, defense... public : Creature(...); virtual ~Creature(); // plein de méthodes Rock & Roll virtuelle // setPosition, attaque, etc... Creature * clone() = 0; }; class MinotaureEnRut : public Creature { protected : // ptet des ptits attributs en plus... public : MinotaureEnRut(...); virtual ~MinotaureEnRut(); // redefinition des méthodes virtuelles pure Creature * clone(); };
Voila, après, créez votre bestiaire et vos créatures avec l'algorithme de la mort et clonez jusqu'a plus soif.
[modifier] Design Patterns Structural
Les DP de structure sont les patterns qui concernent des problèmes d'assemblage d'objets et de structure de code. Nous allons voir 2 DP de cette catégorie, le pattern Façade et le pattern Composite.
[modifier] Composite
[modifier] Qu'est-ce qu'un pattern composite ?
C'est le DP le plus utilisé lorsqu'il s'agit de représenter des structures d'arbres, de composition récursives. Dans le pattern composite, on a trois éléments :
- Le composant : tout les éléments (par exemple dans un arbre) sont des composants.
- Le composite : c'est un composants qui est composé de plusieurs composants appelés "fils"
- La feuille : c'est un composant qui ne possède aucun fils
L'avantage principal du composite c'est de pouvoir traiter de la même manière un objet ou une collection d'objets.
Le schéma UML suivant montre cette structure composant-composite-feuille. Dans ce schéma, la classe client est une foi de plus celle qui est chargé de l'accès à la structure.
Voici maintenant un petit code d'exemple pour illustrer le concept.
// Interface class Composant { protected: std::string nom; public: Composant(std::string n); virtual ~Composant(); virtual void add(Composant * c); virtual void display(unsigned int espace = 0) = 0; }; class Composite : public Composant { protected: std::vector<Composant *> fils; public: Composite(std::string n); virtual ~Composite(); virtual void add(Composant * c); virtual void display(unsigned int espace = 0); }; class Feuille : public Composant { public : Feuille(std::string n); virtual ~Feuille(); virtual void display(unsigned int espace = 0); }; // Implémentation Composant::Composant(std::string n):nom(n) {} Composant::~Composant() {} void Composant::add(Composant * c) {} Composite::Composite(std::string n) :Composant(n), fils() {} Composite::~Composite() { for (std::vector<Composant *> ::iterator i=fils.begin(); i!=fils.end(); i++) { delete *i; } } void Composite::add(Composant * c) { fils.push_back(c); } void Composite::display(unsigned int espace) { for (unsigned int j=0; j < espace; j++) { std::cout << "--"; } std::cout << ' ' << nom << " se compose de :"<< std::endl; for (std::vector<Composant *> ::iterator i=fils.begin(); i!=fils.end(); i++) { (*i)-> display(espace+1); } } Feuille::Feuille(std::string n) :Composant(n) {} Feuille::~Feuille() {} void Feuille::display(unsigned int espace) { for (unsigned int j=0; j < espace; j++) { std::cout << "--"; } std::cout << ' ' << nom << std::endl; } // Exemple d'utilisation int main() { Composant * root = new Composite("Donut's"); Composant * beignet = new Composite("Beignet"); beignet-> add( new Feuille("Farine") ); beignet-> add( new Feuille("Sucre") ); beignet-> add( new Feuille("Oeufs") ); Composant * fourrage = new Composite("Fourrage"); Composant * chocolat = new Composite("Chocolat"); chocolat-> add( new Feuille("Pate de cacao maigre") ); chocolat-> add( new Feuille("Beurre de cacao reconstitue") ); fourrage-> add(chocolat); fourrage-> add( new Feuille("Gelatine") ); fourrage-> add( new Feuille("Colorant E211") ); root-> add(beignet); root-> add(fourrage); root-> display(); return 0; }
Résultat de l'exécution :
Donut's se compose de : -- Beignet se compose de : ---- Farine ---- Sucre ---- Oeufs -- Fourrage se compose de : ---- Chocolat se compose de : ------ Pate de cacao maigre ------ Beurre de cacao reconstitue ---- Gelatine ---- Colorant E211
[modifier] Petites remarques sur le Composite.
Un des problèmes majeurs posé par le DP composite réside dans l'interface de la classe Feuille. Selon le GOF, la classe Feuille doit avoir la même interface que la classe Composant. Mais il y a le problème de la gestion des fils par l'intermédiaire des méthode add et remove. On voit dans le schéma UML et dans le code d'exemple que ces méthodes sont implémenté (vides) dans la classe de base donc non redéfinies dans la classe Feuille. Il est possible de les redéfinir pour par exemple pour déclencher des erreurs si on essaye d'ajouter des fils à une feuille. Aussi, si on veut implémenter une méthode getFils(unsigned int), il faut garder en tête que cette méthode retournera un pointeur nul ou lèvera une exception si elle est appelé sur des Feuilles.
On pourrait imaginer un pattern où l'on aurait que des objets composites, qui lorsqu'il ne contiennent aucun fils, sont considérés comme des feuilles, mais le pattern d'origine permet de redéfinir proprement les comportements des différents objets en fonction de leur types (feuilles ou composite).
Pour conclure, il est vrai que c'est en général très difficile d'avoir une interface semblable entre la feuille et le composant, mais ça vaut la peine d'essayer pour avoir tous les avantages du pattern Composite.
[modifier] Un exemple concret d'utilisation du Composite.
Le composite est le moyen de traiter uniformément des collections d'objets et des objets simples. Si on prend un exemple d'un jeu où l'on peut gérer des unités sous forme uniques mais aussi sous forme de groupes (qui eux-mêmes peuvent être composés de sous groupes, etc...), le composite peut nous simplifier la tâche.
Imaginons que vous gériez un monde remplis de pleins de monstres très vilains qui veulent absolument casser la gueule au gentil héros charismatique. Ces monstres sont répartis sur la carte en groupe ou tout seul. Certains groupes sont constitués de sous groupes (par exemple une armée de monstres repartis en 3 groupes : infanterie, cavalerie, archers). Vous avez envie que lorsque le joueur passe à une certaine distance de ces groupes, ceux ci le pourchassent si ils sont assez rapides pour le faire.
On peut alors penser au code suivant :
//Interface
class Armee // Composant
{
protected :
// Des attributs si besoin
public :
Armee();
virtual ~Armee();
virtual void ajouterArmee(Armee * a);
virtual void enleverArmee(Armee * a);
virtual void guetter(Joueur * j) = 0;
virtual void poursuivre(Joueur * j);
virtual bool peutPoursuivre(Joueur * j) = 0;
virtual bool forceSuffisante(Joueur * j) = 0;
// plein d'autre méthodes qui peuvent être intéressantes
// comme des méthodes pour obtenir des statistiques
// sur l'armée (force globales, etc...)
};
class Groupe : public Armee // Composite
{
protected :
std::vector < Armee * > membres;
// D'autres attributs si besoin (pourquoi pas une position gobale du groupe)
public :
Groupe();
virtual ~Groupe();
virtual void ajouterArmee(Armee * a);
virtual void enleverArmee(Armee * a);
virtual void guetter(Joueur * j); // Si le joueur est assez près et que l'armée
// Peut le poursuivre et que sa force est
// suffisante alors on le poursuit, sinon
// on demande aux armees filles de guetter
// le joueur
virtual void poursuivre(Joueur * j); // Demande à toutes les membres du groupe de
// poursuivre le joueur
virtual bool peutPoursuivre(Joueur *j); // Renvoie vrai si le groupe a la capacité de
// poursuivre le joueur
virtual bool forceSuffisante(Joueur *j); // Renvoie vrai si le groupe a la capacité de
// combattre le joueur
};
class Creature : public Armee // feuille, et aussi classe de base des unités du jeu
{
protected :
// Attributs relatifs aux unités du jeu (pv, force, etc...)
public :
Creature(...);
virtual ~Creature();
virtual void guetter(Joueur * j); // Cette fonction peut même être redéfinie
// par des unités qui dérivent pour gérer
// des champs de visions différents par ex.
virtual void poursuivre(Joueur * j);
virtual bool peutPoursuivre(Joueur * j); // Idem que guetter
virtual bool forceSuffisante(Joueur * j); // Idem que guetter, certaines unités un peut
// kamikazes, renvoie vrai même si leur force
// est pas suffisante
};
Ensuite vous avez une armée contenant tout les monstres de la map, et le joueur se feras poursuivre par des groupes, entiers, des bouts de groupes (par ex: les cavaliers le poursuivent tandis que les archers restent en retrait), des unités toutes seules...
Voila, j'espère que ce code d'exemple vous a aidé à comprendre l'intérêt du DP Composite.
[modifier] Façade
[modifier] Qu'est ce qu'une Façade ?
La façade est un DP qui consiste à donner une interface unifiée et haut niveau à un sous-système composé de plusieurs interfaces et objets aux interactions complexes. Typiquement, la façade peut répondre à des requêtes de l'utilisateur en appelant des méthodes de divers objet du sous-système. L'utilisateur appelle alors des méthodes de la façade sans se soucier du travail effectué par le sous-système.
Un schéma UML pour la route :
Et un code d'exemple :
// Interface
class SousSystemePronom
{
private :
std::vector<std::string> pronoms;
public :
SousSystemePronom();
virtual ~SousSystemePronom();
void afficherPronom();
};
class SousSystemeVerbe
{
private :
std::vector<std::string> verbes;
public :
SousSystemeVerbe();
virtual ~SousSystemeVerbe();
void afficherVerbe();
};
class SousSystemeNom
{
private :
std::vector<std::string> noms;
public :
SousSystemeNom();
virtual ~SousSystemeNom();
void afficherNom();
};
class Facade
{
private :
SousSystemeNom ssnom;
SousSystemeVerbe ssverbe;
SousSystemePronom sspronom;
public :
Facade();
virtual ~Facade();
void afficherPhrase();
void afficherGroupeNominal();
};
// Implementation
SousSystemePronom::SousSystemePronom() :pronoms()
{
pronoms.push_back("le ");
pronoms.push_back("un ");
pronoms.push_back("ce ");
pronoms.push_back("ce connard de ");
pronoms.push_back("l'etrange ");
}
SousSystemePronom::~SousSystemePronom()
{}
void SousSystemePronom::afficherPronom()
{
std::cout << pronoms[ rand() % pronoms.size() ];
}
SousSystemeNom::SousSystemeNom() :noms()
{
noms.push_back("chat ");
noms.push_back("requin ");
noms.push_back("petit vieux ");
noms.push_back("plombier ");
noms.push_back("sosie de Dave ");
}
SousSystemeNom::~SousSystemeNom()
{}
void SousSystemeNom::afficherNom()
{
std::cout << noms[ rand()%noms.size() ];
}
SousSystemeVerbe::SousSystemeVerbe() :verbes()
{
verbes.push_back("a bouffe ");
verbes.push_back("agresse ");
verbes.push_back("martyrise ");
verbes.push_back("drague ");
verbes.push_back("renifle ");
}
SousSystemeVerbe::~SousSystemeVerbe()
{}
void SousSystemeVerbe::afficherVerbe()
{
std::cout << verbes[ rand()%verbes.size() ];
}
Facade::Facade() :ssnom(), ssverbe(), sspronom()
{}
Facade::~Facade()
{}
void Facade::afficherPhrase()
{
sspronom.afficherPronom();
ssnom.afficherNom();
ssverbe.afficherVerbe();
sspronom.afficherPronom();
ssnom.afficherNom();
std::cout << std::endl;
}
void Facade::afficherGroupeNominal()
{
sspronom.afficherPronom();
ssnom.afficherNom();
std::cout << std::endl;
}
// Utilisation
void main()
{
Facade f;
srand(GetTickCount()); // pour aller jusqu'au bout de l'exemple
f.afficherPhrase();
f.afficherPhrase();
f.afficherGroupeNominal();
f.afficherGroupeNominal();
}
Résultat de l'exécution :
un petit vieux agresse l'etrange sosie de Dave le plombier a bouffe ce connard de chat ce chat l'etrange requin
[modifier] Petites remarques sur la Façade
Dans l'exemple ci dessus, pour faire simple, on a considéré que la Façade créait elle même le sous-système qu'elle manipule. Dans le pattern d'origine ce n'est pas toujours le cas. La façade est conçue pour être une adaptation d'interface d'objet existant. Elle peut elle-même créer ses objets ou alors on peut lui lier des objets du sous-système à l'exécution. Il faut donc généralement prévoir des méthodes pour lier les objets du sous-système à la façade.
Le fait que la liaison entre objet et Façade ne se fasse pas seulement à la construction de cette dernière offre un degré de liberté supplémentaire. On peut en effet varier les objets du sous-système à volonté. Par exemple, si on a un système de profiling complexe d'un programme, on peut y accéder par une façade, et selon qu'on veuille stocker les données du profiling dans un fichier, les imprimer ou les afficher sur la sortie standard, on lie la façade à des objets CProfilingFileOut, CProfilingPrinterOut, CProfilingStdOut.
Connaissant tout ça on peut laisser libre cours à son imagination, des Façades qui dérivent d'autres Façades pour enrichir les requêtes disponibles, etc...
[modifier] Un exemple concret d'utilisation d'une Façade
Pour l'exemple concret, on va prendre le cas d'un chargeur de données fichiers, les données à charger sont dans un fichier compressé (allez faire un tour sur le tutorial de bahamut sur l'accès aux fichiers compressés). Il y a différents types de fichiers à charger : des textures, des modèles, des fichiers textes, etc... Tout ces fichier ont un en-tête (écrits par vous même :)) commun pour faciliter le traitement.
On va implémenter une façade pour se simplifier la tache, cette façade auras comme objets associé : un utilitaire pour dézipper l'archive et accéder aux fichier, un objet pour lire les en-têtes, un objet par type de fichiers à charger.
Voici une des interface-implémentation de la façade qu'on pourra utiliser. Les classes du sous-système sont supposées déjà écrites. Ce code est juste fait pour montrer un exemple de structure, l'implémentation des classes annexes n'est pas très importante...
class ChargeurDonnees { private : Unziper unziper; HeaderReader headerReader; ModelReader modelReader; ImageReader imageReader; TextReader textReader; public : ChargeurDonnees(std::string archiveZip); virtual ~ChargeurDonnees(); // Les différentes requetes utilisables : char getTypeFichier(std::string nomFichier); // pour obtenir le type // d'un fichier (à partir de l'en-tête) Model * chargerModel(std::string nomFichier); Image * chargerImage(std::string nomFichier); Text * chargerText(std::string nomFichier); }; ChargeurDonnees::ChargeurDonnees(std::string archiveZip) :unziper(archiveZip), headerReader(), modelReader(), imageReader(), textReader() {} ChargeurDonnees::~ChargeurDonnees() {} char ChargeurDonnees::getTypeFichier(std::string nomFichier) { Fichier * f = unziper.getFichier(nomFichier); Header h = headerReader.getHeader(f); return h.typeFichier; } Model * ChargeurDonnees::chargerModel(std::string nomFichier) { Fichier * f = unziper.getFichier(nomFichier); Header h = headerReader.getHeader(f); return modelReader.getModel(f,h); } Image * ChargeurDonnees::chargerImage(std::string nomFichier) { Fichier * f = unziper.getFichier(nomFichier); Header h = headerReader.getHeader(f); return imageReader.getModel(f,h); } Text * ChargeurDonnees::chargerText(std::string nomFichier) { Fichier * f = unziper.getFichier(nomFichier); Header h = headerReader.getHeader(f); return textReader.getModel(f,h); }
[modifier] Design Patterns Behavioral
Il y a deux types de DP dans la catégorie behavioral. Les patterns orientés Classes qui utilisent les notions d'héritage et de polymorphisme pour décrire des algorithmes et des flux de traitement. Les patterns orientés Objets, eux, décrivent les intéractions dans un groupe d'objets pour, par exemple effectuer certains traitements ne pouvant pas être effectués par un objet seul.
[modifier] Observer
[modifier] Qu'est ce qu'un Observer ?
Le pattern observer vous permet de définir une dépendance entre plusieurs objets, telle que, lorsqu'un des objets (le sujet) change d'état (modification de certains de ses attributs), les autres objets (observeurs) en soit avertis immédiatement.
Voici le schéma UML de ce pattern, si vous ne le comprenez pas tout de suite, n'hésitez pas à aller faire un tour du côté du code d'exemple pour comprendre un peu mieux tout ce bordel.
Et voila le code d'exemple :
//Interface
class ObserveurAbstrait
{
public :
ObserveurAbstrait();
virtual ~ObserveurAbstrait();
virtual void update() = 0;
};
class SujetAbstrait
{
protected :
std::vector<ObserveurAbstrait *> observeurs;
public :
SujetAbstrait();
virtual ~SujetAbstrait();
void attacher(ObserveurAbstrait * o);
void detacher(ObserveurAbstrait * o);
void avertir();
};
class Forum : public SujetAbstrait // Sujet concret
{
protected :
unsigned int nbMessage;
unsigned int nbFlood;
public :
Forum();
virtual ~Forum();
unsigned int getNbMessage();
unsigned int getNbFlood();
void postNewMessage();
void postFlood();
};
class Moderateur : public ObserveurAbstrait // Observeur concret
{
protected :
Forum * sujet;
unsigned int nbFlood;
unsigned int nbMessage;
std::string nom;
public :
Moderateur(Forum * s, std::string n);
virtual ~Moderateur();
virtual void update();
};
// Implémentation
SujetAbstrait::SujetAbstrait() :observeurs()
{}
SujetAbstrait::~SujetAbstrait()
{}
void SujetAbstrait::attacher(ObserveurAbstrait * o)
{
observeurs.push_back(o);
}
void SujetAbstrait::detacher(ObserveurAbstrait * o)
{
std::vector<ObserveurAbstrait *> ::iterator i;
for (i = observeurs.begin(); i!=observeurs.end() && *i != o; i++)
{}
if ( i != observeurs.end() )
{
observeurs.erase(i);
}
}
void SujetAbstrait::avertir()
{
std::vector<ObserveurAbstrait *> ::iterator i;
for (i = observeurs.begin(); i!=observeurs.end(); i++)
{
(*i)-> update();
}
}
ObserveurAbstrait::ObserveurAbstrait()
{}
ObserveurAbstrait::~ObserveurAbstrait()
{}
Forum::Forum()
{
nbMessage = 0;
nbFlood = 0;
}
Forum::~Forum()
{}
unsigned int Forum::getNbFlood()
{
return nbFlood;
}
unsigned int Forum::getNbMessage()
{
return nbMessage;
}
void Forum::postFlood()
{
++nbFlood;
avertir();
}
void Forum::postNewMessage()
{
++nbMessage;
avertir();
}
Moderateur::Moderateur(Forum *s, std::string n) :sujet(s), nom(n)
{
nbFlood = sujet-> getNbFlood();
nbMessage = sujet-> getNbMessage();
}
Moderateur::~Moderateur()
{}
void Moderateur::update()
{
unsigned int nbf = sujet-> getNbFlood();
unsigned int nbm = sujet-> getNbMessage();
std::cout << "Observeur " << nom << " prevenu : ";
std::cout << nbm << " messages et " << nbf << " floods." << std::endl;
if ( nbf != nbFlood )
{
std::cout << "Nouveau Flood detecte..." << std::endl;
}
if (nbm != nbMessage )
{
std::cout << "Nouveau Message detecte..." << std::endl;
}
std::cout << std::endl;
nbFlood = nbf;
nbMessage = nbm;
}
// Utilisation
void main()
{
Forum * f = new Forum;
// Création et attachement des observeurs
Moderateur * sarko = new Moderateur(f,"Sarko");
Moderateur * toto = new Moderateur(f,"Toto");
f-> attacher(sarko);
f-> attacher(toto);
// Changement d'état du sujet
f-> postFlood();
f-> postNewMessage();
// Création et attachement d'un nouvel observeur
Moderateur * charcutier = new Moderateur(f,"Le Charcutier");
f-> attacher(charcutier);
// Detachement d'un observeur
f-> detacher(toto);
// Changement d'état du sujet
f-> postNewMessage();
f-> avertir();
delete f;
delete sarko;
delete toto;
delete charcutier;
}
Résultat de l'exécution :
Observeur Sarko prevenu : 0 messages et 1 floods. Nouveau Flood detecte... Observeur Toto prevenu : 0 messages et 1 floods. Nouveau Flood detecte... Observeur Sarko prevenu : 1 messages et 1 floods. Nouveau Message detecte... Observeur Toto prevenu : 1 messages et 1 floods. Nouveau Message detecte... Observeur Sarko prevenu : 2 messages et 1 floods. Nouveau Message detecte... Observeur Le Charcutier prevenu : 2 messages et 1 floods. Nouveau Message detecte... Observeur Sarko prevenu : 2 messages et 1 floods. Observeur Le Charcutier prevenu : 2 messages et 1 floods.
[modifier] Quelques petites remarques sur l'Observer
Dans le schéma UML du DP, on voit que l'état du sujet est aussi stocké dans l'observeur. C'est facultatif, mais ça peut ouvrir des possibilités comme la détection de ce qui a changé. On peut ainsi effectuer des traitements différents en fonction des changements d'états. Encore une fois, il faut adapter le pattern à votre problème.
Le pattern de l'observer est basé sur un modèle de notification d'état dans lequel le sujet prévient l'observeur en cas de changement. L'inverse de ce système est appelé le "polling", dans ce cas là, l'observeur doit constamment scruter le sujet pour détecter le changement d'état. Pour faire une petit analogie avec le monde réel, vous connaissez sûrement le système de rappel automatique de France Telecom, lorsque c'est occupé chez votre correspondant, vous appuyez sur la touche 5 et vous êtes rappelé automatiquement lorsque sa ligne est libre, c'est un système observeur-observé. Sans ce système on est obligé de rappeler régulièrement pour vérifier si la ligne est libre, c'est du polling.
Un autre avantage non négligeable du pattern observer est son dynamisme. On peut facilement attacher-détacher un observeur en cours d'exécution. Un exemple simple : un bouton dans un GUI peut être considéré comme un observeur d'un objet Entrée-Sortie. Si on veut le désactiver, il suffit de le détacher. On peut bien sûr le rattacher après puis le détacher, puis le rattacher pour faire chier l'utilisateur mais là n'est pas la question...
Il faut bien retenir que la destruction des observeurs ne doit pas être pris en charge par le sujet. La raison est simple, le concept de l'observer est une séparation Observé - Observeur. Deux règles doivent être respectées :
- Le sujet ne sait rien de l'observeur.
- L'observeur NE MODIFIE JAMAIS le sujet
Détruire les observeurs dans le destructeur du sujet (ou dans n'importe quelle méthode exotique d'ailleurs) revient à violer une de ces 2 règles fondamentales.
[modifier] Un exemple concret de l'utilisation de l'Observer
Allez on est reparti pour un tour avec des monstres, un joueur avec une belle armure qui brille et une tronçonneuse avec un réservoir d'essence de 500 litres pour être sûr de ne jamais tomber en panne en plein milieu d'une bonne boucherie à l'ancienne. Ce joueur au cours du jeu traversera divers abattoirs pour trucider du fermier qui engraisse les bovins à la farine transgénique. Et dans chacun de ses abattoirs, il pourras trouver une petite échoppe fort sympathique qui lui proposera d'acheter de l'essence ou divers objets coupants ou contondants.
Maintenant le petit concept intéressant qui va justifier l'emploi de l'observer : On veut que les marchands baissent leurs prix de 10% lorsque le joueur tue 100 fermiers après avoir rencontré le marchand. Je m'explique : le joueur rencontre le marchand A, il a déjà tué 40 fermiers (pas mal!). Il rencontre le marchand B, il en a tué 20 de plus donc 60 en tout. Quand il en auras tué 140, le marchand A lui feras des réductions, et quand il en auras découpés 160 en rondelles, c'est le marchand B qui à son tour baisseras ses prix. Et ainsi de suite, à 240 fermiers tués...
Voici un exemple d'interface pour illustrer le concept...
class Observeur
{
public :
Observeur();
virtual ~Observeur();
virtual void update() = 0;
};
class Sujet
{
protected :
std::vector< Observeur *> observeurs;
public :
Sujet();
virtual ~Sujet();
void attacher(Observeur * o);
void detacher(Observeur * o);
void avertir();
};
class Joueur : public Sujet // Sujet concret
{
protected :
unsigned int nbFermiersTues;
// Plein de trucs
public :
void fermierTue();
// Plein de méthodes
};
class Marchand : public Observeur // observeur concret
{
protected :
unsigned int nbFermiersTues;
Joueur * sujet;
std::vector< Produit *> produits;
// Des tas d'autres attributs
public :
Marchand();
virtual ~Marchand();
void rencontre(Joueur * j);
void update();
// Plein d'autres méthodes
};
// Quelques implementation de methodes
Marchand::Marchand() :Observeur(), produits()
{
nbFermiersTues = 0;
sujet = NULL;
// ...
}
void Marchand::rencontre(Joueur * j)
{
if ( !sujet )
{
sujet = j;
j-> attacher(this);
}
//...
}
void Marchand::update()
{
if (++nbFermiersTues == 100)
{
nbFermiersTues = 0;
std::vector< Produit *> ::iterator i;
for (i=produits.begin(); i!=produits.end(); ++i)
{
(*i)-> reduction(10);
}
}
}
void Joueur::fermierTue()
{
++nbFermiersTues;
avertir();
}
L'avantage de l'observer dans ce cas là, c'est que le joueur n'a pas besoin de maintenir une liste de marchands pour que la mise à jour soit faite. Attention : il existe bien sûr des tas d'implémentations correctes sans utiliser l'observer! Ce n'est qu'un exemple d'illustration...
[modifier] Visitor
[modifier] Qu'est-ce qu'un visitor ?
Le Visitor n'est pas le DP le plus simple, concrètement, il vous permet d'ajouter des traitements à une classe sans avoir à la modifier. Regardez le schéma UML suivant, puis les petites explications en dessous pour comprendre ce DP.
Les classes ElementA et ElementB dérivent de ElementAbstrait, on veut pouvoir leur ajouter des fonctionnalités spécifiques sans perdre l'avantage d'une interface commune bien définie et du polymorphisme. On ne vas donc pas modifier les classes ElementA et ElementB mais on va créer une méthode virtuelle Accepter(Visiteur), le visiteur étant l'objet qui va nous permettre d'effectuer les traitements. On créé une classe VisiteurAbstrait contenant deux méthodes visiterElementA(ElementA) et visiterElementB(ElementB) (méthodes de traitement spécifiques) et on peut ainsi créer des classes Visiteur dérivées qui redéfinissent ces méthodes.
En gros, ajouter une fonctionnalité à la classe ElementA ou ElementB se fait sans les modifiées, juste en créant une nouvelle classe dérivée de Visiteur. Je sais pas si vous me suivez, mais le code d'exemple permettras peut-être d'éclaircir les choses :
// Interface class Visiteur; class VisiteurCuire; class VisiteurRefroidir; class Ingredient; class Sucre; class Pate; class Chocolat; class Cuisinier; //Visiteurs : class Visiteur { public : Visiteur(); virtual ~Visiteur(); virtual void visiterSucre(Sucre * s); virtual void visiterPate(Pate * p); virtual void visiterChocolat(Chocolat * c); }; class VisiteurCuire : public Visiteur { public : VisiteurCuire(); virtual ~VisiteurCuire(); virtual void visiterPate(Pate * p); virtual void visiterChocolat(Chocolat * c); }; class VisiteurRefroidir : public Visiteur { public : VisiteurRefroidir(); virtual ~VisiteurRefroidir(); virtual void visiterChocolat(Chocolat * c); }; // Elements : class Ingredient // ElementAbstrait { protected : std::string nom; public : Ingredient(std::string n); virtual ~Ingredient(); virtual void display() = 0; virtual void accepter(Visiteur * v) = 0; }; class Sucre : public Ingredient { protected : std::string type; public : Sucre(std::string t); virtual ~Sucre(); virtual void display(); virtual void accepter(Visiteur * v); }; class Pate : public Ingredient { public : typedef enum { CUITE = 0, PAS_CUITE } typeCuisson; protected : typeCuisson cuisson; public : Pate(typeCuisson t); virtual ~Pate(); virtual void setCuisson(typeCuisson t); virtual void display(); virtual void accepter(Visiteur * v); }; class Chocolat : public Ingredient { protected : float temperature; public : Chocolat(float t); virtual ~Chocolat(); virtual void setTemperature(float t); virtual void display(); virtual void accepter(Visiteur * v); }; // Client : class Cuisinier { private : std::vector< Ingredient *> beignet; public : Cuisinier(); virtual ~Cuisinier(); void cuire(); void refroidir(); void displayBeignet(); }; // Implémentation Visiteur::Visiteur() {} Visiteur::~Visiteur() {} void Visiteur::visiterChocolat(Chocolat * c) {} void Visiteur::visiterPate(Pate * p) {} void Visiteur::visiterSucre(Sucre * s) {} VisiteurCuire::VisiteurCuire() :Visiteur() {} VisiteurCuire::~VisiteurCuire() {} void VisiteurCuire::visiterChocolat(Chocolat * c) { c-> setTemperature(50.0f); } void VisiteurCuire::visiterPate(Pate * p) { p-> setCuisson(Pate::CUITE); } VisiteurRefroidir::VisiteurRefroidir() :Visiteur() {} VisiteurRefroidir::~VisiteurRefroidir() {} void VisiteurRefroidir::visiterChocolat(Chocolat * c) { c-> setTemperature(4.1f); } Ingredient::Ingredient(std::string n) :nom(n) {} Ingredient::~Ingredient() {} Sucre::Sucre(std::string t) :Ingredient("Sucre"), type(t) {} Sucre::~Sucre() {} void Sucre::display() { std::cout << nom << ' ' << type << std::endl; } void Sucre::accepter(Visiteur * v) { v-> visiterSucre(this); } Pate::Pate(Pate::typeCuisson t) :Ingredient("Pate"), cuisson(t) {} Pate::~Pate() {} void Pate::setCuisson(Pate::typeCuisson t) { cuisson = t; } void Pate::display() { std::cout << nom << ' '; if (cuisson == CUITE) { std::cout << "cuite" << std::endl; } else { std::cout << "pas cuite" << std::endl; } } void Pate::accepter(Visiteur * v) { v-> visiterPate(this); } Chocolat::Chocolat(float t) :Ingredient("Chocolat"), temperature(t) {} Chocolat::~Chocolat() {} void Chocolat::setTemperature(float t) { temperature = t; } void Chocolat::display() { std::cout << nom << " a " << temperature << " degres" << std::endl; } void Chocolat::accepter(Visiteur * v) { v-> visiterChocolat(this); } Cuisinier::Cuisinier() :beignet() { beignet.push_back(new Chocolat(10.5f)); beignet.push_back(new Pate(Pate::PAS_CUITE)); beignet.push_back(new Sucre("Roux")); beignet.push_back(new Sucre("Glace")); } Cuisinier::~Cuisinier() { std::vector< Ingredient *> ::iterator i; for (i=beignet.begin(); i!=beignet.end(); ++i) { delete *i; } } void Cuisinier::cuire() { Visiteur * v = new VisiteurCuire(); std::vector< Ingredient *> ::iterator i; for (i=beignet.begin(); i!=beignet.end(); ++i) { (*i)-> accepter(v); } delete v; } void Cuisinier::refroidir() { Visiteur * v = new VisiteurRefroidir(); std::vector< Ingredient *> ::iterator i; for (i=beignet.begin(); i!=beignet.end(); ++i) { (*i)-> accepter(v); } delete v; } void Cuisinier::displayBeignet() { std::cout << "** Etat du Beignet **" << std::endl; std::vector< Ingredient *> ::iterator i; for (i=beignet.begin(); i!=beignet.end(); ++i) { (*i)-> display(); } std::cout << std::endl; } // Utilisation int main() { Cuisinier c; c.displayBeignet(); c.cuire(); c.displayBeignet(); c.refroidir(); c.displayBeignet(); return 0; }
Résultat de l'exécution :
** Etat du Beignet ** Chocolat a 10.5 degres Pate pas cuite Sucre Roux Sucre Glace ** Etat du Beignet ** Chocolat a 50 degres Pate cuite Sucre Roux Sucre Glace ** Etat du Beignet ** Chocolat a 4.1 degres Pate cuite Sucre Roux Sucre Glace
[modifier] Quelques petites remarques sur le Visitor
Le visitor s'utilise essentiellement lorsqu'on souhaite garder une interface fixe pour un ensemble de classes mais qu'on veut pouvoir quand même créer des traitements spécifiques pour chaque classe tout en :
- Evitant le surpeuplement de méthodes dans les sous-classes
- Donc en évitant d'ajouter à la classe de base des méthodes virtuelles redéfinies non vides par une seule classe fille
- Eviter les magouilles de récupération de types - Cast - Appels de méthodes dans le code
L'ajout de traitement avec un Visitor est très facile, il faut juste créer une nouvelle classe fille de Visitor, et non pas créer une nouvelle méthode dans 50 classes qui dérivent dans tout les sens...
Si vous vous rappelez ce que j'ai ecrit sur le composite, le Visitor est en général utilisé pour être couplé avec ce dernier. On garde ainsi tout les bénéfices d'un composite avec une interface unifiée.
On reproche souvent au Visitor de violer le concept d'encapsulation. D'autres pensent que ce n'est pas forcément nécessaire que toutes les opérations qui peuvent être effectuées sur une classe soient présentes dans celle-ci. On ne connaît pas forcément à l'avance toutes les opérations qu'ont va devoir effectuer sur un objet, le Visitor est donc un moyen de séparer les données et les traitements.
Pour finir, rajoutons qu'il n'est pas nécéssaire de définir des accesseurs pour chaque membres de la classe Element. On peut, au lieu d'appeler le visiteur en lui passant un paramètre Element, l'appeler en lui donnant seulement un ensemble de données membres qui peuvent être intéressantes. Ainsi on ne violeras pas (trop) l'encapsulation.
[modifier] Un exemple concret d'utilisation du Visitor
Pour ce dernier exemple, point besoin de code, on va juste poser l'idée, le code est ultra simple à mettre en place...
Repartons sur le concept présenté dans l'exemple concret d'utilisation d'un composite. On a donc toujours notre armée, divisée en groupe, etc... La classe Feuille est une classe Créature de laquelle tout les types de créatures dérivent. Imaginons maintenant une super nouveauté dans le gameplay, la possibilité d'agir sur les éléments naturels pour handicaper l'ennemi. On peut par exemple faire naître un brouillard qui diminue le champ de vision des ennemis (sauf les archers, ils ont 12/10 à chaques oeils), ou alors faire péter un orage, ce qui a pour effet de foutre les jetons aux chevaux, diminution de la vitesse des unités de cavaleries.
Vous avez saisi le truc? Un VisiteurBrouillard et un VisiteurOrage, ils parcourent l'armée et agissent donc sur les unités en fonction de leur types et on ne se casse pas le cul à faire une méthode brouillard() dans Armee, redéfinie dans Groupe, redéfinie dans Créature, etc... Et hop! Le visitor nous a simplifié la vie!
[modifier] À venir
Plein de patterns Rock&Roll, le DP FactoryMethod aussi appelé "constructeur virtuel", le Builder, le Mediator, le State, etc. Que de bonheur en perspective !
[modifier] Références
Ce document a été publié sur la version 3 du G.C.N. par TheClems.
- Auteur Original : TheClems
- Date de publication : 19 août 2004








(aucun commentaire actuellement)