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

Présentation des principaux design patterns en C++


précédentsommairesuivant

II. Clonage

Définition : un prototype est une classe dont le but est d'être clonée.

II-1. Mise en situation et première solution

La sémantique des classes en C++ se décompose principalement selon les classes de valeur et les classes d'entité. Les premières sont copiables, assignables mais ne font pas partie d'une hiérarchie d'héritage. A l'inverse, les secondes sont souvent utilisées dans un but de polymorphisme dynamique et ne sont pas copiable. Néanmoins, il y'a des cas où on a besoin de copier ces objets.

Pour rendre le concept plus clair, supposons une classe SGBDConnexion dont hérite publiquement les classes MySQLConnexion et AccesConnexion. A cause du LSP, partout où on attend une objet de type SGBDConnexion, on pourra passer un objet de type MySQLConnexion ou AccesConnexion. Comment peut on faire pour copier un objet qui a été casté en objet du type SGBDConnexion dans ces conditions ?

 
Sélectionnez
class SGBDConnexion 
{
public:
 virtual ~SGBDConnexion() =0;
};

SGBDConnexion::~SGBDConnexion(){}

class MySQLConnexion: public SGBDConnexion
{
 MySQLDriver m_driver;
public:
 virtual ~MySQLConnexion(){}
};
class AccesConnexion: public SGBDConnexion
{
 AccesDriver m_driver;
public:
 virtual ~AccesConnexion(){}
};
void foo(SGBDConnexion& a)
{
//comment peut on copier l'objet a ?
}

La solution semble se trouver dans les fonctions virtuelles. En effet, si A dispose d'une fonction virtuelle clone qui copie l'objet courant, alors on pourra dans les classes B et C redéfinir la fonction pour qu'elle copie un objet de type B ou C.

On arrive donc au schéma UML suivant:

Diagramme UML du DP prototype½
Diagramme UML du pattern prototype
 
Sélectionnez
//on complétant les classes du dessus, on a:
class SGBDConnexion 
{
public:
SGBDConnexion* clone () const =0;
};
class MySQLConnexion: public SGBDConnexion 
{
public:
MySQLConnexion* clone () const {return new MySQLConnexion(*this);}
};
class AccesConnexion: public SGBDConnexion
{
public:
AccesConnexion* clone () const {return new AccesConnexion(*this);}
};

Que se passe-t-il ?

Dans un premier temps, on définit une fonction clone virtuelle pure qui renvoie un pointeur de type SGBDConnexion. Pourquoi un pointeur et non une référence ? Pour ne pas retourner une référence sur un objet temporaire, référence qui ne serait plus valide une fois l'exécution de la fonction finie.

Ensuite, dans les classes dérivées, le type de retour de la fonction change. Le compilateur devrait en théorie m'insulter de tous les nom. Mais il n'en n'est rien. En effet, on utilise ici les retours covariants. Pour faire simple les valeurs de retour covariantes sont les cas où la valeur de retour d'une fonction virtuelle est une référence ou un pointeur sur une classe C. Ainsi les classes qui redéfiniront cette fonction virtuelle pourront renvoyer un pointeur ou une référence sur une classe dérivée de C.

II-2. Améliorations

Ce code présente deux problèmes majeurs: il renvoie des pointeurs bruts (une calamité à gérer) et présente un certain nombre de redondance dans les définitions des fonctions clone.

La première amélioration que l'on peut faire est d'utiliser des boost::shared_ptr pour éviter d'avoir des pointeurs bruts. Mais alors on ne peut plus utiliser les retours covariants. En effet, il n'y a aucun lien d'héritage entre shared_ptr<SGBDConnexion> et shared_ptr<MySQLConnexion>. On va donc renvoyer partout des shared_ptr<SGBDConnexion>.

 
Sélectionnez
class SGBDConnexion 
{

public:
    virtual ~SGBDConnexion() =0;
    virtual const shared_ptr<SGBDConnexion> clone() const =0;
    
};

SGBDConnexion::~SGBDConnexion(){}

class MySQLConnexion: public SGBDConnexion
{
public:
    virtual ~MySQLConnexion(){} 
    virtual const shared_ptr<SGBDConnexion> clone() const
    {
       return shared_ptr<SGBDConnexion>( new MySQLConnexion(*this));
    }
};
class AccesConnexion: public SGBDConnexion
{
public:
    virtual ~AccesConnexion(){}
    virtual const shared_ptr<SGBDConnexion> clone() const 
    {
	return  shared_ptr<SGBDConnexion>(new AccesConnexion(*this));
    }
};

void foo(SGBDConnexion& a)
{
    shared_ptr<SGBDConnexion> ptr = a.clone();
}

On se retrouve donc avec un code fonctionnel mais qui dispose de certaines redondances au niveau du code de clone: seul le type d'objet diffère. On peut donc factoriser ce code dans une fonction template. Reste le problème de l'endroit où l'on va définir cette fonction. On peut soit la définir en tant que fonction libre, soit en tant que fonction membre statique. L'avantage de cette dernière option est qu'on peut limiter la visibilité de la fonction aux classes dérivées.

 
Sélectionnez
class SGBDConnexion
{
//. comme avant
protected:
    template <class T> static shared_ptr<SGBDConnexion> do_clone(const T* obj) 
    {
	return shared_ptr<SGBDConnexion>( new T(*obj));
    }    
};

class MySQLConnexion: public SGBDConnexion
{
public:
    virtual ~MySQLConnexion(){}
    virtual const shared_ptr<SGBDConnexion> clone() const 
    {
	return  do_clone(this);
    }
};

class AccesConnexion: public SGBDConnexion
{
public:
    virtual ~AccesConnexion(){}
    virtual const shared_ptr<SGBDConnexion> clone() const 
    {
	return  do_clone(this);
    }
};

Grâce à la déduction automatique des arguments templates, le code réel de clone est identique en apparence. Mais néanmoins, il existe une grande différence, tous les pointeurs this ont un type différent, c'est pourquoi on ne peut pas factoriser le code de la fonction membre clone dans la classe SGBDConnection.

II-3. Application

Cette technique du clonage permet d'améliorer les performances dans un certain nombres de cas. En effet, certains objets (comme un connexion à une base de données) sont lourd à construire: il faut se connecter à la base puis s'identifier, procédure plutôt coûteuse. Si on a besoin de travailler sur une autre table d'une même base, il suffit de cloner la connexion existante et de modifier la table sur laquelle on travaille. La copie pouvant se faire quasiment bit-par-bit, le compilateur est en mesure d'effectuer ceci très rapidement. A la fin de l'opération de clonage, on se retrouve alors avec 2 connexions opérationnelles mais dont le temps de construction aura été plus rapide que si on avait reconstruit toute la connexion depuis zéro. On retrouve la même idée avec la fonction fork sur les systèmes Un*x.


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.