原型模式(Prototype Pattern)是一种创建型设计模式,它的核心思想是用一个已经创建的实例作为原型,通过复制该原型来创建新的对象,而不必通过 new 直接实例化。下面我会从定义、结构、应用场景、C++实现、深/浅拷贝问题等方面为你详细讲解。
1. 定义与意图
意图: 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
简单说,就是给系统增加一个"克隆"能力,让一个对象可以自我复制。当直接创建对象的代价较高(比如初始化很耗时、依赖外部资源、或者类数量庞大)时,原型模式可以让客户端代码不依赖具体类就能生成新对象。
GoF 中的描述: 通过"原型"实例指定要创建的对象类型,然后拷贝该原型创建更多的同类型对象。
2. 结构图与参与者
原型模式通常包含以下几种角色:
-
Prototype(抽象原型类)
声明一个克隆自身的接口,通常是
clone()方法。 -
ConcretePrototype(具体原型类)
实现
clone()方法,完成自身的复制。 -
Client(客户端)
让一个原型对象克隆自己,从而得到一个新的对象。客户端只需知道抽象原型接口,不需要知道具体类。
此外,很多时候会引入一个原型管理器(Prototype Registry),用来存储和查找已有的原型实例,客户端通过键值来获取原型副本。
3. 工作原理
原型模式的核心就是对象的拷贝。拷贝分为两种:
-
浅拷贝(Shallow Copy)
只复制对象本身,不复制其引用的成员。也就是说,拷贝对象中的指针/引用与原对象指向同一块内存。对于值类型成员没问题,但对动态分配的资源(如堆内存、文件句柄等)会造成多个对象共享同一资源,可能导致多次释放、重复修改等问题。
-
深拷贝(Deep Copy)
不仅复制对象本身,还递归复制其引用的所有对象。拷贝出的对象与原对象完全独立,互不影响。
原型模式通常要求实现深拷贝 ,保证克隆体完全独立。
在 C++ 中,需要特别注意拷贝构造函数和 clone() 的实现。
4. 应用场景
什么时候使用原型模式?
-
对象创建代价很高
初始化一个对象需要耗费大量资源(如复杂的计算、数据库读取、网络请求、大量配置等),而之后的对象与某个已有对象"相似"。这时可以先创建一个原型,后续通过克隆来快速生成。
-
系统要独立于它的产品类
客户端不需要知道具体产品类的名字,只需要通过原型克隆。这在框架设计中很常见,例如游戏中的怪物生成器,只需向原型管理器注册怪物原型,运行时根据配置文件或关卡数据动态克隆。
-
当一个类有很多子类,且这些子类的差异仅在状态组合
不需要为每种组合写一个子类,可以先构造具有特定状态的原型,然后克隆它们。
-
动态加载与配置
配合原型管理器,可以在程序运行时动态注册、删除原型,实现高度灵活的对象创建。例如:在图形编辑器中,用户选定一个图形,然后通过克隆该图形来"复制-粘贴"。
-
避免大量工厂类
当使用工厂方法模式可能导致大量平行工厂类时,原型模式可以替代,将工厂角色合并到对象自身的
clone()方法中。
5. C++ 代码示例
下面给出一个完整的 C++ 例子,包含抽象原型、具体原型、深拷贝处理以及原型管理器。
5.1 抽象原型类
cpp
#include <iostream>
#include <string>
#include <memory>
#include <unordered_map>
// 抽象原型
class Shape {
public:
virtual ~Shape() = default;
virtual std::unique_ptr<Shape> clone() const = 0; // 克隆接口
virtual void draw() const = 0;
};
这里使用 std::unique_ptr 来表达所有权转移,确保克隆后由调用者管理内存。
5.2 具体原型类(以圆形和矩形为例)
cpp
// 具体原型 ------ 圆形
class Circle : public Shape {
private:
int x, y;
int radius;
std::string color;
public:
Circle(int x, int y, int r, const std::string& c)
: x(x), y(y), radius(r), color(c) {}
// 拷贝构造函数 (深拷贝)
Circle(const Circle& other)
: x(other.x), y(other.y), radius(other.radius), color(other.color) {
std::cout << "Circle deep-copied.\n";
}
std::unique_ptr<Shape> clone() const override {
// 利用拷贝构造函数进行深拷贝
return std::make_unique<Circle>(*this);
}
void draw() const override {
std::cout << "Circle at (" << x << ", " << y
<< ") radius=" << radius << " color=" << color << std::endl;
}
};
// 具体原型 ------ 矩形
class Rectangle : public Shape {
private:
int x, y;
int width, height;
std::string color;
public:
Rectangle(int x, int y, int w, int h, const std::string& c)
: x(x), y(y), width(w), height(h), color(c) {}
Rectangle(const Rectangle& other)
: x(other.x), y(other.y), width(other.width),
height(other.height), color(other.color) {
std::cout << "Rectangle deep-copied.\n";
}
std::unique_ptr<Shape> clone() const override {
return std::make_unique<Rectangle>(*this);
}
void draw() const override {
std::cout << "Rectangle at (" << x << ", " << y
<< ") width=" << width << " height=" << height
<< " color=" << color << std::endl;
}
};
5.3 原型管理器(可选)
cpp
class ShapeRegistry {
private:
std::unordered_map<std::string, std::unique_ptr<Shape>> prototypes;
public:
// 注册原型
void addPrototype(const std::string& key, std::unique_ptr<Shape> prototype) {
prototypes[key] = std::move(prototype);
}
// 通过键克隆一个对象
std::unique_ptr<Shape> createShape(const std::string& key) const {
auto it = prototypes.find(key);
if (it != prototypes.end()) {
return it->second->clone();
}
return nullptr;
}
};
5.4 客户端使用
cpp
int main() {
// 创建原型管理器并注册原型
ShapeRegistry registry;
registry.addPrototype("big red circle",
std::make_unique<Circle>(10, 20, 30, "red"));
registry.addPrototype("small blue rect",
std::make_unique<Rectangle>(0, 0, 40, 20, "blue"));
// 根据需求克隆对象
auto shape1 = registry.createShape("big red circle");
if (shape1) shape1->draw();
auto shape2 = registry.createShape("small blue rect");
if (shape2) shape2->draw();
// 克隆另一个圆形
auto shape3 = registry.createShape("big red circle");
if (shape3) shape3->draw();
return 0;
}
输出:
Circle deep-copied.
Circle at (10, 20) radius=30 color=red
Rectangle deep-copied.
Rectangle at (0, 0) width=40 height=20 color=blue
Circle deep-copied.
Circle at (10, 20) radius=30 color=red
可以看出,每次 createShape() 都调用 clone() 并触发了拷贝构造函数,实现深拷贝。
5.5 深拷贝关键点
如果 Circle 或 Rectangle 内部含有动态分配的资源(比如 char* 或指向其它对象的指针),深拷贝必须:
- 在拷贝构造函数中为新对象分配独立的资源;
- 复制原对象资源的内容;
- 确保析构函数能正确释放自己的资源,避免重复释放。
在 C++11 之后,推荐使用 RAII 和智能指针(如 std::unique_ptr、std::string)来管理资源,可以大幅简化深拷贝工作。大多数标准库类型(vector, string 等)的拷贝构造本身就是深拷贝,我们只需要确保成员对象都正确实现了拷贝语义。
6. 优缺点
优点:
- 减少子类构造:可以不通过大量工厂类或子类来创建配置复杂的对象。
- 动态配置:运行时可以随时增删原型,比编译时静态生成更灵活。
- 隐藏对象创建细节:客户端只与抽象原型接口打交道,降低耦合。
- 效率提升:在初始化成本高的情况下,克隆比重新初始化快得多。
缺点:
- 深拷贝复杂:对象有多重引用或循环引用时,实现深拷贝可能很困难。
- 每个子类必须实现 clone:如果系统中有大量具体类,每个都要正确实现 clone 方法,增加维护成本。
- 破坏单例/不可复制语义:某些对象设计上不应被复制,却因为原型模式不得不暴露克隆接口。
7. 与其它模式的关系
- 抽象工厂模式 可以用原型模式来实现,工厂通过克隆产品原型来生产新品。
- 组合模式 和 装饰模式 经常与原型一起使用,你可能会克隆一个复杂组合结构。
- 原型模式有时可以替代 备忘录模式 的部分功能,将对象状态保存在克隆体中,但它们的意图和复杂度不同。
8. 原型模式、拷贝构造函数和复制赋值运算符
它们不是平级替代的关系,而是目的与手段的关系。
下面我从几个关键维度来剖析它们的异同。
8.1 核心定义与意图的不同
-
拷贝构造函数 (Copy Constructor)
- 意图: 是一种语言层面的对象构造机制。它定义了如何用同类型的已存在对象来初始化一个新对象。核心关注点是"正确复制资源"。
- 特点: 是静态的、编译时确定的。
ClassName obj2(obj1); - 解决的问题: 主要是深拷贝与浅拷贝问题,防止悬垂指针或多次释放同一块内存。
-
复制赋值运算符 (Copy Assignment Operator)
- 意图: 是一种语言层面的对象赋值机制。它定义了如何将同类型的已存在对象的值,复制给另一个同样已存在的对象。核心关注点是"赋值"和"释放旧资源"。
- 特点: 是静态的、编译时确定的。
obj2 = obj1; - 解决的问题: 同上,但额外要处理自我赋值和释放已有资源的问题。
-
原型模式 (Prototype Pattern)
- 意图: 是一种设计层面的对象创建模式。它通过一个原型实例来指定要创建对象的类型,并通过复制这个原型来创建新对象。
- 特点: 目标是解耦。让客户端代码不知道具体要创建什么类,只知道要克隆的那个"原型"对象。
- 解决的问题: 类太多、初始化复杂、希望运行时动态配置对象种类等设计问题,而不是单纯的内存拷贝问题。
8.2 多态性:原型模式的关键区别
这是原型模式区别于前两者最本质的特征。拷贝构造函数和复制赋值运算符都不具备多态性。
cpp
#include <memory>
class Base {
public:
// 普通拷贝构造:从同类型对象构造
Base(const Base& other) { /* ... */ }
// 这是一个"虚拷贝构造"的替代方案,即原型模式的核心
virtual std::unique_ptr<Base> clone() const = 0;
};
class Derived : public Base {
public:
Derived(const Derived& other) : Base(other) { /* ... */ }
// 实现原型模式的clone接口
std::unique_ptr<Base> clone() const override {
// 先调用自身的拷贝构造,再封装成基类指针返回
return std::make_unique<Derived>(*this);
}
};
int main() {
// 1. 多态创建:客户端只知道Base*,却能正确克隆出Derived对象
std::unique_ptr<Base> prototype = std::make_unique<Derived>();
std::unique_ptr<Base> cloneObj = prototype->clone(); // 这是原型模式
// 2. 拷贝构造做不到这一点
// Base* bPtr = new Derived();
// Base* bPtr2 = new Base(*bPtr);
// 错误!因为编译器只知道 *bPtr 是 Base&,会调用Base的拷贝构造,发生"对象切片"
return 0;
}
结论: 原型模式通过 clone() 虚函数实现了多态复制。你可以在完全不知道对象具体类型的情况下,通过基类指针或引用来复制它。这是任何非虚的拷贝构造或赋值运算符都做不到的。
8.3 与"组合模式"的协同工作
原型模式能递归处理复杂结构,而单纯的拷贝构造通常只在单层类中自动工作。
考虑之前图形编辑器的例子:
cpp
class CompoundGraphic : public Graphic {
std::vector<std::unique_ptr<Graphic>> children;
public:
// 拷贝构造函数只能处理同类型
CompoundGraphic(const CompoundGraphic& other) {
for (const auto& child : other.children) {
// 在这里,child是std::unique_ptr<Graphic>,我们不知道每个child具体是Dot还是Circle
// 但我们可以调用 clone(),因为这是Graphic基类的接口
children.push_back(child->clone()); // 原型模式决定性的一刻!
}
}
// ...
};
在这里,CompoundGraphic自己的拷贝构造函数 内部,使用了其成员的原型模式接口。单纯的拷贝构造无法完成这种异构集合的深拷贝。
8.4 对比总结表
| 特性 | 拷贝构造 / 复制赋值 | 原型模式 (clone) |
|---|---|---|
| 核心意图 | 正确复制值 | 多态创建对象,解耦具体类 |
| 多态性 | 无(非虚函数,静态绑定) | 有(虚函数,动态绑定) |
| 应用层次 | 语言特性,用于类的实现 | 设计模式,用于系统架构 |
| 客户端依赖 | 依赖具体类名 | 依赖抽象原型接口 (Base*) |
| 动态配置 | 不支持 | 支持(运行时注册原型) |
| 典型场景 | 类的内部实现细节 | 框架、对象工厂、编辑器 |
| 相互关系 | clone()方法的实现基础 |
利用拷贝构造来实现,并赋予其多态能力 |
简单类比:
拷贝构造函数 是造砖的模具,决定了每块砖怎么造出来。
原型模式是拿一块"样品砖"给流水线,流水线(客户端)不需要知道这是什么类型的砖,只要它能"自我复制"就行。而"自我复制"这个能力,很多时候就是用拷贝构造函数这个"模具"实现的。
因此,你在项目中的思维路径应该是:
- 设计时: 如果发现系统需要"用一个对象来快速创建一批相似对象",或者"客户端不想用一堆if-else和new来对付各种子类",就应该考虑引入原型模式。
- 实现时: 为每个具体子类实现
clone()函数,而这个函数的内部实现 ,最佳实践就是利用C++的拷贝构造函数return std::make_unique<Derived>(*this);。