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

Présentation des principaux design patterns en C++


précédentsommairesuivant

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

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

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

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

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

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

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

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

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

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

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


précédentsommairesuivant

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2007 Côme David. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.