Fondements du Patron Prototype
Le patron de conception Prototype, classé parmi les patrons créationnels du Gang of Four (GoF), offre un mécanisme permettant de générer de nouvelles instances en dupliquant un objet existant. Cette approche s'avère particulièrement pertinente lorsque l'instanciation d'un objet via un constructeur classique (et l'opérateur new) s'avère trop coûteuse en ressources, ou lorsque le système doit s'affranchir des classes concrètes de ses produits.
En C++, l'implémentation de ce patron est intrinsèquement liée à la sémantique de copie du langage. Elle exige une maîtrise rigoureuse des constructeurs de copie, des opérateurs d'assignation, et impose de faire un choix architectural clair entre la copie superficielle (shallow copy) et la copie profonde (deep copy) pour éviter les fuites de mémoire ou les corruptions de pointeurs pendants.
Architecture et Mécanisme de Clonage
La structure classique repose sur une interface commune, matérialisée par une classe abstraite, qui déclare une méthode virtuelle pure (généralement nommée clone). Les classes concrètes héritent de cette interface et redéfinissent cette méthode. L'implémentation la plus robuste en C++ consiste à appeler le constructeur de copie de la classe dérivée au sein de la méthode clone, assurant ainsi une réplication exacte de l'état de l'objet. Cette approche équivaut conceptuellement à l'implémentation de l'interface ICloneable dans d'autres langages orientés objet.
Cas d'Usage Stratégiques
- Détermination dynamique des types : Lorsque la classe exacte de l'objet à instancier n'est connue qu'à l'exécution, le clonage polymorphique permet de créer de nouvelles instances sans recourir à des blocs conditionnels complexes ou à des fabriques lourdes.
- Préservation et restauration d'état : Pour sauvegarder l'état d'un objet à un instant précis (snapshot) et permettre des opérations de type "undo" ou "rollback", dupliquer l'objet est souvent plus sûr et plus rapide que de sérialiser puis désérialiser ses données.
- Optimisation des performances : Si la création d'un objet implique des opérations intensives (requêtes réseau, parsage de fichiers volumineux, calculs cryptographiques), il est préférable d'initialiser un prototype une seule fois, puis de le cloner pour les utilisations ultérieures.
- Simplification de l'initialisation : Lorsque plusieurs objets partagent une configuration de base complexe avec seulement quelques variations mineures, cloner un objet pré-configuré et modifier uniquement les attributs divergents réduit considérablement la verbosité du code.
Implémentation Polymorphique Moderne
L'exemple suivant illustre un clonage polymorphique utilisant les fonctionnalités du C++ moderne, notamment les pointeurs intelligents (std::unique_ptr) pour garantir une gestion de la mémoire sans fuite. Il corrige également les erreurs de transtypage (casting) souvent rencontrées dans les implémentations naïves.
#include <iostream>
#include <memory>
#include <string>
class Shape {
protected:
std::string identifier;
std::string geometryType;
public:
virtual ~Shape() = default;
// Interface de clonage pur
virtual std::unique_ptr<Shape> clone() const = 0;
void setId(const std::string& id) { identifier = id; }
std::string getId() const { return identifier; }
std::string getType() const { return geometryType; }
};
class Circle : public Shape {
private:
double radius;
public:
Circle() {
geometryType = "Circle";
radius = 0.0;
}
// Constructeur de copie pour le clonage
Circle(const Circle& source) {
geometryType = source.geometryType;
identifier = source.identifier;
radius = source.radius;
}
void setRadius(double r) { radius = r; }
std::unique_ptr<Shape> clone() const override {
return std::make_unique<Circle>(*this);
}
};
int main() {
Circle originalShape;
originalShape.setId("C-001");
originalShape.setRadius(15.5);
// Création d'un nouvel objet par clonage
std::unique_ptr<Shape> clonedShape = originalShape.clone();
clonedShape->setId("C-002");
std::cout << "Original -> ID: " << originalShape.getId()
<< ", Type: " << originalShape.getType() << std::endl;
std::cout << "Clone -> ID: " << clonedShape->getId()
<< ", Type: " << clonedShape->getType() << std::endl;
return 0;
}
Gestion d'États Complexes et Configuration
Lorsqu'un objet encapsule des conteneurs dynamiques ou des états internes multiples, le prototype permet de dupliquer l'intégralité de la configuration courante. L'exemple ci-dessous démontre la réplication d'un profil de configuration serveur, où la modification de l'instance clonée n'affecte en aucun cas le prototype d'origine.
#include <iostream>
#include <string>
#include <vector>
class ServerConfiguration {
private:
std::string hostName;
int portNumber;
std::vector<std::string> firewallRules;
public:
ServerConfiguration(std::string host, int port)
: hostName(host), portNumber(port) {}
// Utilisation du constructeur de copie par défaut (deep copy pour std::vector et std::string)
ServerConfiguration(const ServerConfiguration& other) = default;
void setHost(const std::string& host) { hostName = host; }
void addRule(const std::string& rule) { firewallRules.push_back(rule); }
void displayStatus() const {
std::cout << "Serveur: " << hostName << " | Port: " << portNumber << "\nRegles: ";
for (const auto& rule : firewallRules) {
std::cout << "[" << rule << "] ";
}
std::cout << "\n------------------------\n";
}
// Méthode de clonage classique retournant un pointeur brut
ServerConfiguration* clone() const {
return new ServerConfiguration(*this);
}
};
int main() {
ServerConfiguration prodBase("prod-node-01", 443);
prodBase.addRule("ALLOW_HTTPS");
prodBase.addRule("DROP_ALL");
// Clonage de la configuration de production pour créer un environnement de staging
ServerConfiguration* stagingNode = prodBase.clone();
stagingNode->setHost("staging-node-01");
stagingNode->addRule("ALLOW_SSH");
prodBase.displayStatus();
stagingNode->displayStatus();
delete stagingNode;
return 0;
}