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 dubliquer 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
recupè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.