IV. La fabrique▲
Fabrique : classe dont le rôle est de créer d'autres objets.
IV-A. Un besoin de fabrique▲
Il est courant en C++ d'avoir comme design une hiérarchie de classes dérivant toutes d'une même interface pour profiter du polymorphisme. Mais gérer manuellement la création d'objets peut vite être 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-B. Première implémentation▲
Avant toute chose, il faut préciser comment seront créés les objets. En effet, sur ce point, plusieurs stratégies sont envisageables telle la création via new ou celle par clonage. Pour ma part, j'ai choisi celle basée sur le clonage. Mais vous trouverez facilement d'autres implémentations sur le web.
D'après l'analyse et la remarque précédente, on peut écrire une première version de la fabrique comme suit :
fabrique.h
#
ifndef
FABRIQUE_H
#
define
FABRIQUE_H
#
include
<map>
#
include
<string>
//
la
classe
prototype
abordé
en
II
template
<
class
T>
class
Prototype
{
public
:
virtual
~
Prototype
(){
}
virtual
T*
Clone
() const
=
0
;
}
;
//
l'interface
"Figure"
class
Figure : public
Prototype<
Figure>
{
public
:
virtual
void
SeDessiner
()=
0
;
}
;
class
Carre : public
Figure
{
public
:
Figure*
Clone
() const
;
void
SeDessiner
();
}
;
class
Cercle : public
Figure
{
public
:
Figure*
Clone
() const
;
void
SeDessiner
();
}
;
//
La
fabrique
à
proprement
parler
class
FactoryFigure
{
public
:
static
std::
map<
std::
string,Figure*
>
m_map;
public
:
//
Fonction
qui
associe
clé
<=>
prototype
static
void
Register
(const
std::
string&
key,Figure*
obj);
//
Celle
qui
va
créer
les
objets
Figure*
Create
(const
std::
string&
key) const
;
}
;
#
endif
Et le fichier fabrique.cpp
#
include
<iostream>
#
include
<iterator>
#
include
"fabrique.h"
using
namespace
std;
Figure*
Carre::
Clone
() const
{
return
new
Carre
(*
this
);
}
void
Carre::
SeDessiner
()
{
cout<
<
"
Je
suis
un
Carre
"
<
<
endl;
}
Figure*
Cercle::
Clone
() const
{
return
new
Cercle
(*
this
);
}
void
Cercle::
SeDessiner
()
{
cout<
<
"
Je
suis
un
Cercle
"
<
<
endl;
}
std::
map<
string,Figure*
>
FactoryFigure::
m_map=
std::
map<
string,Figure*
>
();
void
FactoryFigure::
Register
(const
string&
key,Figure*
obj)
{
//
si
la
clé
n'est
pas
déjà
présente
if
(m_map.find
(key)=
=
m_map.end
())
{
//
on
ajoute
l'objet
dans
la
map
m_map[key]=
obj;
}
//
on
pourrait
détruire
obj
mais
cette
tâche
ne
revient
pas
à
Register
}
Figure*
FactoryFigure::
Create
(const
std::
string&
key) const
{
Figure*
tmp=
0
;
std::
map<
string, Figure*
>
::
const_iterator it=
m_map.find
(key);
//
si
l'itérateur
ne
vaut
pas
map.end(),
cela
signifie
que
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-C. Un code plus réutilisable avec les templates▲
Comme on l'a vu, une fabrique est liée à deux choses : le type d'objet qu'elle va créer et la clé qu'elle va utiliser pour les indexer. De ce fait, il 'suffit' de transformer Figure et std::string en deux paramètres pour une classe template afin d'obtenir une très grande modularité.
En effet, avec les templates, on peut indexer n'importe quel type d'objet avec n'importe quoi (des ints, des strings...). De ce fait, le code de la fabrique devient :
//
fichier
fabrique.h
template
<
class
Object,class
Key=
string>
class
Factory
{
static
std::
map<
Key,Object*
>
m_map;
public
:
static
void
Register
(Key key,Object*
obj);
Object*
Create
(const
Key&
key);
}
;
//
fichier
fabrique.cpp
template
<
class
Object,class
Key>
std::
map<
Key,Object*
>
Factory<
Object,Key>
::
m_map=
std::
map<
Key,Object*
>
();
template
<
class
Object,class
Key>
void
Factory<
Object,Key>
::
Register
(Key key,Object*
obj)
{
if
(m_map.find
(key)=
=
m_map.end
())
{
m_map[key]=
obj;
}
}
template
<
class
Object,class
Key>
Object*
Factory<
Object,Key>
::
Create (const
Key&
key)
{
Object*
tmp=
0
;
typename
std::
map<
Key, Object*
>
::
iterator it=
m_map.find
(key);
if
(it!
=
m_map.end
())
{
tmp=
((*
it).second)-
>
Clone
();
}
return
tmp;
}
Et son utilisation est comme celle d'un vecteur :
Fabrique<
Figure>
fac;
La seule 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.