II. Prototype▲
Définition : un prototype est une classe dont le but est d'être clonée.
II-A. Exemple de besoin du protoype▲
Pendant la création de vos programmes, vous serez sans doute amenés à instancier des objets de taille conséquente en mémoire.
Créer un gros objet ne pose guère de problèmes. Mais en créer plusieurs à la suite peut amener à tuer les performances de votre application.
La solution est alors de copier l'objet de base, le prototype, puis de modifier ce qui doit l'être pour que le nouvel objet réponde aux besoins.
une autre utilisation est la possibilité qu'offre le clonage pour dupliquer des objets polymorphes. En effet pour utiliser le constructeur de copie, il faut connaître le type réel de l'objet, ce qui n'est plus le cas dès qu'on utilise le polymorphisme de dynamique en C++.
De cette façon, on s'aperçoit que le pattern prototype dispose d'une méthode obligatoire, Clone, et d'une facultative Change. La première a pour but d'effectuer une copie du prototype tandis que la seconde a pour rôle de changer l'état de l'objet.
Ainsi, le diagramme UML qui modélise le pattern prototype est :
II-B. Exemple naïf d'implémentation▲
D'après l'analyse précédente et en prenant comme exemple une connexion Mysql, une première implémentation de ce pattern pourrait être tout simplement :
//fichier prototype.h
#ifndef PROTOTYPE_H
#define PROTOTYPE_H
#include
<string>
class
Prototype
{
public
:
virtual
~
Prototype();
virtual
Prototype*
Clone() const
=
0
;
}
;
class
MySQLManager: public
Prototype
{
std::
string m_host,m_login,m_pass;
std::
string m_base,m_table;
public
:
MySQLManager(const
std::
string&
host=
""
,const
std::
string&
login=
""
,const
std::
string&
pass=
""
);
MySQLManager*
Clone() const
;
void
Afficher();
void
Set(const
std::
string&
base=
""
,const
std::
string&
table=
""
);
}
;
#endif
Et dans le fichier prototype.cpp
#include
<iostream>
#include
"prototype.h"
Prototype::
~
Prototype()
{
}
MySQLManager::
MySQLManager(const
std::
string&
host,const
std::
string&
login,const
std::
string&
pass):
m_host(host),m_login(login),m_pass(pass)
{
//on se connecte à la base Mysql.
//ceci prend du temps, car il faut se connecter puis s'identifier.
//Lourd.
}
MySQLManager*
MySQLManager::
Clone() const
{
// le constructeur de recopie fait tout le travail à notre place.
return
(new
MySQLManager(*
this
));
}
void
MySQLManager::
Set(const
std::
string&
base,const
std::
string&
table)
{
m_base =
base;
m_table =
table;
//on se connecte a la base m_base pour étudier m_table
}
void
MySQLManager::
Afficher() const
{
std::
cout<<
m_host<<
"|"
<<
m_login<<
"|"
<<
m_host<<
"|"
<<
m_pass<<
"|"
<<
m_base<<
"|"
<<
m_table<<
std::
endl;
}
int
main(void
)
{
MySQLManager manager1("localhost"
,"Davidbrcz"
,"motdepasse"
);
manager1.Set("faussebase"
,"table1"
);
//ici manager1 est connecté sur localhost avec l'identifiant Davidbrcz sur la table table1 de faussebasse
manager1.Afficher();
//Maintenant, on doit travailler en parallèle sur table2 de faussebase2 toujours sur localhost.
//on crée donc un autre manager qu'on récupère avec un std::auto_ptr pour éviter de gérer la mémoire en cas de problème.
std::
auto_ptr<
MySQLManager>
manager2(manager1.Clone());
//et ici manager2 est déjà connecté à localhost avec l'identifiant Davidbrcz.Pas besoin de se réidentifier.
manager2->
Afficher();
manager2->
Set("faussebase2"
,"table2"
);
manager2->
Afficher();
//on travaile ..
return
0
;
}
Ce code mérite quelques explications.
Tout d'abord on déclare une classe Prototype qui est une interface grâce à la méthode virtuelle pure Clone. Ensuite on déclare une classe MySQLManager qui hérite de Prototype.
On rend cette classe instanciable en définissant la méthode Clone. Mais la fonction virtuelle Prototype::Clone renvoie un Prototype. Or le type de retour ne rentre normalement pas dans la définition d'une fonction. Le compilateur devrait donc en théorie me crier dessus.
Mais si ceci compile sans problème c'est grâce aux valeurs de retour covariantes.
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.
C'est ce qu'il se passe ici. MySQLManager dérive de Prototype donc MySQLManager::Clone peut renvoyer un pointeur vers une instance de MySQLManager.
Mais le problème ici, c'est que Clone renvoie un pointeur brut, utiliser un pointeur intelligent pour le gérer n'est pas du tout obligatoire. Si l'utilisateur utilise un pointeur brut pour gérer la mémoire, ceci peut poser des problèmes pour récupérer la mémoire en cas d'exception.
II-C. Implémentation avec les templates▲
Les retours covariants ne marchent pas avec des pointeurs intelligents. Pour contourner cela, on doit utiliser des templates
Ainsi les classes Prototype et MySQLManager deviennent :
//fichier prototypetpl.h
#ifndef PROTOTYPE_H
#define PROTOTYPE_H
#include
<string>
#include
<memory>
template
<
class
T>
class
Prototype
{
public
:
virtual
~
Prototype(){}
virtual
std::
auto_ptr<
T>
Clone() const
=
0
;
}
;
class
MySQLManager: public
Prototype<
MySQLManager>
{
std::
string m_host,m_login,m_pass;
std::
string m_base,m_table;
public
:
MySQLManager(const
std::
string host=
""
,const
std::
string login=
""
,const
std::
string pass=
""
);
void
Afficher() const
;
std::
auto_ptr<
MySQLManager>
Clone() const
;
void
Set(const
std::
string base=
""
,const
std::
string table=
""
);
}
;
#endif
Et le fichier prototype.cpp (seul change Clone, le reste est identique)
std::
auto_ptr<
MySQLManager>
MySQLManager::
Clone() const
{
return
std::
auto_ptr<
MySQLManager>
(new
MySQLManager(*
this
));
}
int
main(void
)
{
//main ne change pas, on est juste sûr que la mémoire sera libérée en cas de problème
}
L'utilisation des templates permet de simuler des retours covariants avec les pointeurs intelligents. Ceci assure une plus grande résistance au programme.