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