一、背景
在软件系统中,有些对象的创建过程较为复杂,而且有时候需要频繁创建,原型模式通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的办法创建出更多同类型的对象,克隆出的新对象不会改变、影响原型对象。通过复制该原型对象,可以避免构造函数的复杂过程,这个过程可能包含有比较耗时的系统调用、数据库操作等。
二、模式定义
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。这这个定义中,最重要的一个词是“拷贝”,也就是口头上的复制,而这个拷贝,也就是原型模式的精髓所在。
模式分析
- 该创建的对象,还是要创建的,原型模式并不会减少创建对象的个数,只是会简化创建对象的流程。
- 原型模式要求对象实现一个可以克隆自身的接口(类型)。这样一来,通过原型实例创建新的对象,就不需要关心这个实例本身的类型,只需要实现克隆自身的方法,也而无需再去通过new来创建。
- 原型模式最大的优点就是如果你想要在运行时候,获取某个对象的全部或者部分状态,可以获取一份内容相同的实例。
三、模式角色和UML类图
抽象原型(Prototype):提供一个克隆自身的接口
具体原型(ConcretePrototype):实现抽象原型的clone接口,一般调用自己的拷贝复制函数,注意浅拷贝和深拷贝。
客户端(Clinet):客户端面向抽象编程,只需要知道对象的抽象类型,调用其clone函数实现对象拷贝。
始终记住,直接new出来的对象,只含有状态,原型模式产出的对象,可以在运行期间获取运行对象的某些状态,这是重中之重。
Prototype是对象原型,定义了克隆自身的接口clone(),有具体的派生类实现;ConcretePrototype1类和ConcretePrototype2类继承自Prototype类,并实现Clone接口,实现克隆自身的操作。同时,在ConcretePrototype1类和ConcretePrototype2类中需要重写默认的复制构造函数,供Clone函数调用,Clone就是通过在内部调用重写的复制构造函数实现的。在后续的扩展中,如果某个类要实现Clone功能,就只需要继承Prototype类,然后重写自己的默认复制构造函数就好了。
拷贝构造函数:拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象
代码示例
有一个系统中有很多系统配置和用户简况:
初始读取配置或用户简况需要花一些时间(比如用一些系统调用或读取数据库等),但并非实时数据,只需初始化读一编;因为众多系统配置和用户简况需要初始化,每次手动初始化比较繁琐,希望能一个类其中管理并快速创建实例那么如何能不每次手动初始化对象,并能克隆初始化的数据到新的实例呢?
使用原型模式,第一次花一段时间初始化系统配置和用户简况的数据,存入相应的变量内;原型管理类其中管理需要克隆的类,直接克隆初始化好的实例的数据变量值,不再需要去或系统调用或读取来初始化数据。客户直接使用原型管理类获取实例,不再需要手动实例化它们。
/* 原型模式 */
class Prototype {
public:
virtual ~Prototype() {}
virtual Prototype* Clone() = 0;
};
/* 系统配置类继承原型基类 */
class Configuration : public Prototype {
private:
string file_information_;
public:
virtual ~Configuration() {};
void GetFileInformation() {
cout << "takes 5 seconds to get information" << endl;
file_information_ = "Long file information";
}
virtual Prototype* Clone() {
Configuration *c = new Configuration();
c->file_information_ = file_information_;
return c;
}
void ShowInformation() {
cout << "Showing " << file_information_ << endl;
}
};
/* 用户简况类继承原型基类 */
class UserProfile : public Prototype {
private:
string database_information_;
public:
virtual ~UserProfile() {};
void GetDatabaseInformation() {
cout << "take 5 seconds to get database information" << endl;
database_information_ = "Long database information";
}
virtual Prototype* Clone() {
UserProfile *u = new UserProfile();
u->database_information_ = database_information_;
return u;
}
void ShowInformation() {
cout << "Showing " << database_information_ << endl;
}
};
/* 类似对象工厂 */
class PrototypeManager {
private:
map<int, Prototype*> *prototype_map_;
public:
PrototypeManager() {
prototype_map_ = new map<int, Prototype*>;
}
virtual ~PrototypeManager() {
delete prototype_map_;
}
/* AddPrototype 添加原型实例 */
void AddPrototype(Prototype *p, int index) {
(*prototype_map_)[index] = p;
}
/* 克隆原型实例返回新的实例 */
Prototype* GetPrototype(int index) {
return prototype_map_->at(index)->Clone();
}
};
int main(int argc, char *argv[]) {
Configuration *c = new Configuration();
c->GetFileInformation();
UserProfile *u = new UserProfile();
u->GetDatabaseInformation();
PrototypeManager *manager = new PrototypeManager();
manager->AddPrototype(c, 0);
manager->AddPrototype(u, 1);
Configuration *config_clone = dynamic_cast<Configuration *>(manager->GetPrototype(0));
config_clone->ShowInformation();
UserProfile *user_profile_clone = dynamic_cast<UserProfile *>(manager->GetPrototype(1));
user_profile_clone->ShowInformation();
delete user_profile_clone;
delete config_clone;
delete manager;
delete u;
delete c;
}
四、模式总结
适用场景
- 当我们的对象类型不是开始就能确定,而是在运行期确定的,那么我们通过这个类型的对象克隆出一个新的对象比较容易一些,主要是为了克隆出这个对象在某个运行时刻的某些状态值。
- 有的时候,我们需要一个对象在某个状态下的副本,此时,我们使用原型模式是最好的选择;例如:一个对象,经过一段处理之后,其内部的状态发生了变化;这个时候,我们需要一个这个状态的副本,如果直接new一个新的对象的话,但是它的状态是不对的,此时,可以使用原型模式,将原来的对象拷贝一个出来,这个对象就和之前的对象是完全一致的了。
- 当我们处理一些比较简单的对象时,并且对象之间的区别很小,可能就几个属性不同而已,那么就可以使用原型模式来完成,省去了创建对象时的麻烦了。
- 类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等
- 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用
优点
- 原型模式最大的优点就是如果你想要在运行时候,获取某个对象的全部或者部分状态,可以获取一份内容相同的实例。
- 性能提高、逃避构造函数的约束。
缺点
- 在实现深克隆时需要编写较为复杂的代码。