La factory


La Factory, brrr

Dans ce tutorial, je vais tenter d'aborder le design pattern factory (fabrique pour les anglophobes).
A mon habitude, nous allons retrouver la même ligne directrice que pour les précédents tutoriaux.
Je vais commencer par expliquer brièvement le principe de la factory, ensuite en découlera une première implémentation en C++ et enfin on essayera de rendre ça le plus générique possible.

Definition

La factory est en fait un objet dont le rôle est de créer des instances d'autres objets.
Dans un modeleur par exemple, on pourrait avoir une factory d'objets géométriques qui servirait à créer des sphères, cônes, etc...

Première Implémentation :

Nous allons prendre le cas d'une factory d'objets géométriques dont le but est de fournir des objets héritant de l'interface Shape.

class Shape{...};

De plus, nous avons dans l'application des objets implémentant l'interface shape comme Sphere, Cone, ....

class Sphere : public Shape{...};
class Cone   : public Shape{...};

Une premiere implémentation de la factory pourrait donner ceci :

class ShapeFactory{
public:
  Shape * createSphere(void){
    return new Sphere();
  }
  Shape * createCone(void){
    return new Cone();
  }
};

Ce qui donne à l'utilisation :

ShapeFactory factory;
 
Shape * sphere = factory.createSphere();
sphere->display();

Cette première approche fonctionne, mais a le désavantage de rendre le code de la factory dépendant de l'application.
Nous allons tenter de définir la factory de manière générique (template ;) ) afin de pouvoir la réutiliser dans d'autres cas.

On templatise un peu ?

On peut voir la factory comme une map : à une clef donnée, on associe un type d'objet a créer.

On devrait donc avoir au niveau des paramètres de la factory générique, le type de la clef et le type d'objet que la factory retourne. Ceci nous donne le prototype suivant :

template  class Factory;

Et à l'utilisation :

Factory shapeFactory;

De plus, on s'attend à pouvoir écrire quelque chose du genre :

Shape *sphere = shapeFactory.create("Sphere");

On enrichit donc notre esquisse de la classe :

template  class Factory{
public:
  Object * create(key);
}

Pour le corps de la méthode create, nous avons besoin de réellement créer une instance d'un objet que la factory ne connait pas. Comme ceci ne peut pas se faire par magie, il va falloir enregistrer dans la factory une fonction capable de créer chaque objet et les associer à une clef.

template  class Factory{
public:
// un pointeur de fonction qui retourne un Object*
  typedef Object *(*Creator)(void);
 
  map _registeredCreator;
 
  bool register(Key key, Creator creator){
    if(_registeredCreator.find(key) != _registeredCreator.end())
      return false; // la clef est deja utilisé
    _registeredCreator.insert(pair(key, creator));
    return true;
  }
 
  Object * create(Key key){
    Object *object;
    Creator creator;
    map::iterator it;
 
    // on cherche le pointeur de fonction associé a la clef
    it = _registeredCreator.find(key);
    if(it == _registeredCreator.end())
	return NULL; // on ne l'a pas trouvé
 
    // on récupère le pointeur de fonction
    creator = (*it).second;
    // on appelle la fonction pour créer un nouvel objet
    object = (*creator)();
    // on retourne l'objet
    return object;
  }
};

Regardons maintenant comment utiliser tout ça :

// des fonctions de création d'objets
Shape *createSphere(void){return new Sphere;}
Shape *createCone(void){return new Cone;}
 
Factory shapeFactory;
shapeFactory.register("Sphere", createSphere);
shapeFactory.register("Cone", createCone);
 
Shape *sphere = shapeFactory.create("Sphere");

Ici le problème reste la nécessité d'avoir des fonctions "C" à mettre dans le code, et de devoir enregistrer ces fonctions à la main. Donc à chaque ajout d'objet géométrique devoir aller remodifier ce code d'initialisation de la factory.

Pour rendre ceci un peu plus propre, nous allons définir ces fonctions en statique dans chaque objet géométrique et se servir de l'initialisation des CRT pour enregistrer les fonctions automatiquement.

// sphere.h
class Sphere : public Shape{
  static Shape* create(void);
  static bool register;
};
// sphere.pp
#include "ShapeFactory.h" // on considère que la shapeFactory est une variable globale accessible par ce fichier
 
Shape * Shpere::create(void){
  return new Sphere();
}
// auto enregistrement :
bool Sphere::register = shapeFactory.register("Sphere", Sphere::create);

Il est possible d'utiliser des macros pour encore réduire la taille du code à produire.

Petite explication : au lancement du programme, la variable statique register est initialisée. Cette initialisation passe par l'appel de la methode shapeFactory::register qui enregistre le "create".

Conlusion:

J'ai ici présenté l'implémentation "générique" d'une factory; pour pousser un peu plus l'exemple du modeleur, on peut imaginer que l'interface Shape permette de décrire des attributs (association nom / valeur). Une sphère par exemple aura un paramètre de type float nommé "radius".
Notre petite application graphique aura à parcourir les entrées de la factory pour y découvrir tous les objets "instanciables" et fournir à l'utilisateur un panel de boutons pour créer des Shapes.
A partir de là, à chaque Shape créé, elle pourra construire une boîte de dialogue d'édition adaptée à l'objet (à la mode 3DS), SANS en connaitre le type réel.

Pour aller plus loin :

Il est possible de s'abstraire de l'écriture de la fonction create statique dans les classes en utilisant les templates pour créer des objets dont le rôle est de remplacer la fonction statique.

A l'utilisation cela donne quelque chose comme :

bool Shape::register = shapeFactory.register ("Shape", Creator() );

Il faut pour cela modifier un peu la classe factory, mais je vous le laisse en exercice ;) (j'ai toujours révé de dire ça un jour...)

Dernier point, on peut utiliser les singleton pour ne pas avoir à stocker la factory dans une variable globale.

joyeuses fetes ;)

Twxs

Cet article provient du tutorial publié par Twxs le 24/12/2004 sur l'ancien site de Coder-Studio.

, ,

  1. No comments yet.
(will not be published)