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 ?
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:
//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>.
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.
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.