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 :
IV-2. Première implémentation▲
Avant toute chose, il faut préciser comment seront créés les objets. En effet, sur ce point, plusieurs stratégies sont envisageables telle la création via new ou celle par clonage. Pour ma part, j'ai choisi celle basée sur le clonage. Mais vous trouverez facilement d'autres implémentations sur le web.
D'après l'analyse et la remarque précédente, on peut écrire une première version de la fabrique comme suit :
fabrique.h
#ifndef FABRIQUE_H
#define FABRIQUE_H
#include
<map>
#include
<string>
//la classe prototype abordé en II
template
<
class
T>
class
Prototype
{
public
:
virtual
~
Prototype(){}
virtual
T*
Clone() const
=
0
;
}
;
//l'interface "Figure"
class
Figure : public
Prototype<
Figure>
{
public
:
virtual
void
SeDessiner()=
0
;
}
;
class
Carre : public
Figure
{
public
:
Figure*
Clone() const
;
void
SeDessiner();
}
;
class
Cercle : public
Figure
{
public
:
Figure*
Clone() const
;
void
SeDessiner();
}
;
//La fabrique à proprement parler
class
FactoryFigure
{
public
:
static
std::
map<
std::
string,Figure*>
m_map;
public
:
//Fonction qui associe clé <=> prototype
static
void
Register(const
std::
string&
key,Figure*
obj);
//Celle qui va créer les objets
Figure*
Create(const
std::
string&
key) const
;
}
;
#endif
Et le fichier fabrique.cpp
#include
<iostream>
#include
<iterator>
#include
"fabrique.h"
using
namespace
std;
Figure*
Carre::
Clone() const
{
return
new
Carre(*
this
);
}
void
Carre::
SeDessiner()
{
cout<<
"Je suis un Carre"
<<
endl;
}
Figure*
Cercle::
Clone() const
{
return
new
Cercle(*
this
);
}
void
Cercle::
SeDessiner()
{
cout<<
"Je suis un Cercle"
<<
endl;
}
std::
map<
string,Figure*>
FactoryFigure::
m_map=
std::
map<
string,Figure*>
();
void
FactoryFigure::
Register(const
string&
key,Figure*
obj)
{
//si la clé n'est pas déjà présente
if
(m_map.find(key)==
m_map.end())
{
//on ajoute l'objet dans la map
m_map[key]=
obj;
}
//on pourrait détruire obj mais cette tâche ne revient pas à Register
}
Figure*
FactoryFigure::
Create(const
std::
string&
key) const
{
Figure*
tmp=
0
;
std::
map<
string, Figure*>
::
const_iterator it=
m_map.find(key);
//si l'itérateur ne vaut pas map.end(), cela signifie que que la clé à été trouvée
if
(it!=
m_map.end())
{
tmp=
((*
it).second)->
Clone();
}
//on pourrait lancer une exeption si la clé n'a pas été trouvée
return
tmp;
}
int
main(void
)
{
//notre fabrique
FactoryFigure fac;
//on enregistre des types
FactoryFigure::
Register("Carre"
,new
Carre);
FactoryFigure::
Register("Cercle"
,new
Cercle);
//on crée des objets via la fabrique
Figure *
c=
fac.Create("Cercle"
);
Figure *
ca=
fac.Create("Carre"
);
c->
SeDessiner();
ca->
SeDessiner();
delete
c;
delete
ca;
return
0
;
}
Ce code ne présente aucune grande difficulté. Pour chaque clé, on associe un prototype que la méthode Create se charge de cloner, rien de plus.
Mais ce code est très mauvais du point de vue de la conception.
En effet, la fabrique est intimement liée au type d'objet qu'elle va créer. De ce fait, pour créer une autre fabrique, il faut copier-coller du code ce qui est très mauvais signe.
IV-3. Un code plus réutilisable avec les templates▲
Comme on l'a vu, une fabrique est liée à deux choses : le type d'objet qu'elle va créer et la clé qu'elle va utiliser pour les indexer.
De ce fait, il 'suffit' de transformer Figure et std::string en deux paramètres pour une classe template afin d'obtenir une très grande modularité.
En effet, avec les templates, on peut indexer n'importe quel type d'objet avec n'importe quoi (des ints, des strings...). De ce fait, le code de la fabrique devient :
//fichier fabrique.h
template
<
class
Object,class
Key=
string>
class
Factory
{
static
std::
map<
Key,Object*>
m_map;
public
:
static
void
Register(Key key,Object*
obj);
Object*
Create(const
Key&
key);
}
;
//fichier fabrique.cpp
template
<
class
Object,class
Key>
std::
map<
Key,Object*>
Factory<
Object,Key>
::
m_map=
std::
map<
Key,Object*>
();
template
<
class
Object,class
Key>
void
Factory<
Object,Key>
::
Register(Key key,Object*
obj)
{
if
(m_map.find(key)==
m_map.end())
{
m_map[key]=
obj;
}
}
template
<
class
Object,class
Key>
Object*
Factory<
Object,Key>
::
Create (const
Key&
key)
{
Object*
tmp=
0
;
typename
std::
map<
Key, Object*>
::
iterator it=
m_map.find(key);
if
(it!=
m_map.end())
{
tmp=
((*
it).second)->
Clone();
}
return
tmp;
}
Et son utilisation est comme celle d'un vecteur :
Fabrique<
Figure>
fac;
La seule vrai innovation est la présence du typename devant l'itérateur. Pour sa justification reportez-vous à la FAQ. En ce qui concerne l'initialisation de m_map, elle n'a pas changée, elle est juste passé à la version template qui fait un peu plus peur mais qui est exactement la même qu'avant, à la généricité près.