一.意图
Prototype是一种创建性设计模式,允许你复制现有对象,而不使代码依赖于它们的类。
在软件系统中,经常面临着"某些结构复杂的对象"的创建工作;由于需求的变化,这些对象经常面临着剧烈的变化,但是他们却拥有比较稳定一致的接口。
使用原型实例指定对象的种类,然后通过拷贝这些原型来创建新对象。------《设计模式》GoF
二.问题
假设你有一个对象,你想创建一个它的精确复制品。你会怎么做?首先,你必须创建一个同类的新对象。然后你必须逐一查看原始对象的所有字段,并将其值复制到新对象上。
不错!但有个条件。并非所有对象都能以这种方式复制,因为某些对象的字段可能是私有的,外部看不到。

直接做法还有一个问题。因为你必须知道对象的类才能创建复制品,你的代码就依赖于那个类。如果额外的依赖不让你害怕,还有另一个陷阱。有时你只知道对象所遵循的接口,却不知道具体的类,例如,当某个方法中的参数接受任何遵循某个接口的对象时。
三.解决方案
原型模式将克隆过程委托给被克隆的实际对象。该模式为所有支持克隆的对象声明了一个通用接口。这个界面允许你克隆一个对象,而不必将代码与该对象的类耦合。通常,这样的接口只包含一个方法。clone
该方法的实现在所有类别中都非常相似。该方法创建当前类的对象,并将旧对象的所有字段值带入新对象。你甚至可以复制私有字段,因为大多数编程语言允许对象访问同类其他对象的私有字段。clone
支持克隆的对象称为原型。当你的对象有几十个字段和数百种可能配置时,克隆它们可能是子类化的替代方案。

预建原型可以作为子分类的替代方案。
它是这样运作的:你创建一组对象,配置方式各异。当你需要像你配置的那样的对象时,只需克隆一个原型,而不是从零开始构建新对象。
四.现实世界的类比
在现实生活中,原型机用于在开始批量生产前进行各种测试。但在这种情况下,原型机不参与任何实际生产,而是扮演被动角色。

由于工业原型并不真正自我复制,更接近这种模式的类比是有丝分裂细胞分裂过程(还记得吗?)。有丝分裂后,形成一对相同的细胞。原始细胞作为原型,积极参与复制的创建。
五.结构
基本实现

原型注册表实现

六.适合应用场景
-
当你的代码不应该依赖于你需要复制的具体对象类时,就用原型模式。
当你的代码通过某种接口从第三方代码传递给你的对象时,这种情况经常发生。这些对象的具体类是未知的,即使你想依赖也无法依赖它们。
原型模式为客户端代码提供了一个通用接口,用于处理所有支持克隆的对象。该接口使客户端代码独立于其克隆的具体对象类。
-
当你想减少只在初始化对象方式上不同的子类数量时,可以使用这个模式。
假设你有一个复杂的类,需要繁琐的配置才能使用。配置这个类有几种常见方式,这些代码散布在你的应用中。为了减少重复,你创建多个子类,并将所有常见配置代码放入它们的构造子中。你解决了复制问题,但现在有很多假子职业。
原型模式允许你使用一组以不同方式配置的预建物体作为原型。客户端无需实例化与某个配置匹配的子类,只需寻找合适的原型并克隆它。
七.实现方式
-
创建原型接口并在其中声明方法。或者如果你有现有类层级,也可以把这个方法添加到所有类中。
clone -
原型类必须定义接受该类对象作为参数的替代构造器。构造函数必须将流传对象中类别中定义的所有字段的值复制到新创建的实例中。如果你在更改子类,必须调用父构造子,让超类处理其私有字段的克隆。
如果你的编程语言不支持方法重载,你就无法创建单独的"原型"构造函数。因此,必须在方法内将对象数据复制到新创建的克隆中。不过,把这些代码放在普通构造器中更安全,因为调用作符后,生成的对象会被完整配置返回。
clone``new -
克隆方法通常仅包含一行:运行一个作符,使用原型构造函数版本。注意,每个类必须显式覆盖克隆方法,并使用自己的类名和作符。否则,克隆方法可能会生成父类的对象。
new``new -
可选地,创建集中式原型注册库,存储常用原型目录。
你可以把注册表作为新的工厂类实现,或者用静态方法把它放到基础原型类里。该方法应根据客户端代码传递给方法的搜索条件搜索原型。这些条件可以是简单的字符串标签,也可以是复杂的搜索参数集合。找到合适的原型后,注册表应克隆该原型并返回给客户端。
最后,将对子类构造子的直接调用替换为对原型注册表的工厂方法调用。
八.优缺点
-
优点:
-
你可以克隆对象而不与它们的具体类耦合。
-
你可以省去重复的初始化代码,转而克隆预建的原型。
-
你可以更方便地制造复杂的物体。
-
在处理复杂对象的配置预设时,你会得到继承的替代方案。
-
-
缺点
- 代码可能会变得比应有的更复杂,因为会随着模式引入许多新的接口和类。
九.与其他模式的关系
-
许多设计从工厂方法开始(简单且通过子职业更可定制),随后逐步发展为抽象工厂、原型或建造者(更灵活但更复杂)。
-
抽象工厂类通常基于一组工厂方法,但你也可以用Prototype来组合这些类的方法。
-
原型在你需要把命令副本保存到历史时很有帮助。
-
大量使用复合材料和装饰器的设计通常可以从Prototype中受益。应用图案可以让你克隆复杂结构,而不是从零重建。
-
原型机不基于继承,所以没有缺点。另一方面,Prototype需要对克隆对象进行复杂的初始化。工厂方法基于继承,但不需要初始化步骤。
-
有时候Prototype可以成为Memento更简单的替代品。如果你想存储在历史中的对象状态相当简单,没有外部资源链接,或者链接容易重新建立,这种方法就适用。
-
抽象工厂、建造者和原型都可以作为单例实现。
十.示例代码
#include <iostream>
#include <cstring>
using namespace std;
class Prototype {
public:
virtual ~Prototype() {}
virtual Prototype* clone() const = 0;
virtual void showInfo() const = 0;
};
// 具体原型类:包含堆内存成员 char* name
class ConcretePrototype : public Prototype {
private:
char* name; // 堆内存成员,必须深拷贝
int age;
public:
// 构造函数:分配堆内存
ConcretePrototype(const char* n, int a) : age(a) {
name = new char[strlen(n) + 1];
strcpy(name, n);
}
// 拷贝构造函数:深拷贝核心 - 给新对象分配独立的堆内存
ConcretePrototype(const ConcretePrototype& other) {
age = other.age;
// 深拷贝:重新开辟堆内存,拷贝内容,而非地址
name = new char[strlen(other.name) + 1];
strcpy(name, other.name);
}
// 析构函数:释放堆内存,防止内存泄漏
~ConcretePrototype() {
delete[] name;
}
// 克隆方法:依然调用拷贝构造,但此时是深拷贝
Prototype* clone() const override {
return new ConcretePrototype(*this);
}
void showInfo() const override {
cout << "姓名:" << name << ",年龄:" << age << endl;
}
// 修改堆内存成员,验证深拷贝效果
void setName(const char* n) {
delete[] name;
name = new char[strlen(n) + 1];
strcpy(name, n);
}
};
int main() {
Prototype* prototype = new ConcretePrototype("张三", 20);
cout << "原型对象:";
prototype->showInfo();
Prototype* cloneObj = prototype->clone();
cout << "克隆对象:";
cloneObj->showInfo();
// 修改克隆对象的堆内存成员
((ConcretePrototype*)cloneObj)->setName("李四");
cout << "修改后-克隆对象:";
cloneObj->showInfo();
cout << "修改后-原型对象:";
prototype->showInfo(); // 原型不受影响,堆内存独立
delete prototype;
delete cloneObj;
return 0;
}
执行结果
原型对象:姓名:张三,年龄:20
克隆对象:姓名:张三,年龄:20
修改后-克隆对象:姓名:李四,年龄:20
修改后-原型对象:姓名:张三,年龄:20