原型模式:模式动机与定义
说实话,原型模式是我个人非常喜欢的一个创建型模式。为什么?因为它解决了一个很实际的问题——当你需要创建大量相似对象时,直接 new 的成本太高了。
我刚开始做 C++ 开发那会儿,接手过一个游戏引擎项目。里面要生成成千上万的粒子特效,每个粒子都有自己的位置、速度、颜色。如果每个粒子都走一遍构造函数,性能直接崩掉。后来我用了原型模式,效果立竿见影。
原型模式的动机其实很简单:通过复制一个已有对象来创建新对象,而不是通过实例化类。说白了,就是找个模板,直接拷贝一份。
它的定义是这样的:
原型模式(Prototype Pattern):用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。
嗯,这里要注意一点。原型模式的核心在于「拷贝」这两个字。但拷贝这件事,在 C++ 里可没那么简单。这就引出了我们下一个话题——深拷贝与浅拷贝。
深拷贝与浅拷贝
我在项目中遇到过不少因为拷贝问题导致的 bug。有一次,一个同事用默认拷贝构造函数复制了一个对象,结果两个对象共享了同一块内存。改了一个,另一个也跟着变了。排查了一下午才发现问题。
为什么会这样?因为 C++ 的默认拷贝是浅拷贝。
浅拷贝
浅拷贝只复制对象本身,不复制对象内部指针指向的资源。两个对象的指针成员指向同一块内存。
class ShallowCopy {
public:
int* data;
ShallowCopy(int val) {
data = new int(val);
}
// 默认拷贝构造函数 — 浅拷贝
// ShallowCopy(const ShallowCopy& other) : data(other.data) {}
};
你想想看,如果其中一个对象析构时释放了 data,另一个对象的 data 就成了悬空指针。这就是典型的「双删」问题。
深拷贝
深拷贝则不同。它会复制对象本身,同时为指针成员分配新的内存,并复制指针指向的内容。
class DeepCopy {
public:
int* data;
DeepCopy(int val) {
data = new int(val);
}
// 深拷贝构造函数
DeepCopy(const DeepCopy& other) {
data = new int(*other.data);
}
~DeepCopy() {
delete data;
}
};
我个人习惯在实现原型模式时,一律使用深拷贝。除非你能百分之百确定对象内部没有动态资源,否则浅拷贝就是一颗定时炸弹。
警告:浅拷贝在多线程环境下尤其危险。我曾经因为浅拷贝导致两个线程同时修改同一块数据,程序崩溃得毫无征兆。
C++ 代码实现
好,我们来看看原型模式在 C++ 里怎么落地。核心思路是:基类提供一个 Clone() 纯虚函数,每个派生类自己实现深拷贝。
#include <iostream>
#include <string>
#include <vector>
// 抽象原型
class Shape {
public:
virtual ~Shape() {}
virtual Shape* Clone() const = 0;
virtual void Draw() const = 0;
};
// 具体原型:圆形
class Circle : public Shape {
private:
int radius;
std::string color;
public:
Circle(int r, const std::string& c) : radius(r), color(c) {}
// 拷贝构造函数(深拷贝)
Circle(const Circle& other) : radius(other.radius), color(other.color) {}
Shape* Clone() const override {
return new Circle(*this);
}
void Draw() const override {
std::cout << "画一个" << color << "的圆形,半径:" << radius << std::endl;
}
};
// 具体原型:矩形
class Rectangle : public Shape {
private:
int width;
int height;
std::string color;
public:
Rectangle(int w, int h, const std::string& c)
: width(w), height(h), color(c) {}
Rectangle(const Rectangle& other)
: width(other.width), height(other.height), color(other.color) {}
Shape* Clone() const override {
return new Rectangle(*this);
}
void Draw() const override {
std::cout << "画一个" << color << "的矩形,宽:" << width
<< ",高:" << height << std::endl;
}
};
// 客户端代码
int main() {
// 创建原型
Circle circlePrototype(10, "红色");
Rectangle rectPrototype(20, 15, "蓝色");
// 通过克隆创建新对象
Shape* shape1 = circlePrototype.Clone();
Shape* shape2 = rectPrototype.Clone();
Shape* shape3 = circlePrototype.Clone(); // 再克隆一个圆形
shape1->Draw();
shape2->Draw();
shape3->Draw();
// 清理
delete shape1;
delete shape2;
delete shape3;
return 0;
}
这段代码里,我用了拷贝构造函数来实现深拷贝。每个派生类都实现了自己的 Clone() 方法,返回 new Circle(*this) 或 new Rectangle(*this)。这样客户端只需要调用 Clone(),完全不用关心具体类型。
小技巧:如果你用 C++11 或更高版本,可以考虑返回 std::unique_ptr<Shape> 来自动管理内存。这样能避免手动 delete 的麻烦。
原型模式的知识体系
下面这张图可以帮你快速理解原型模式的核心结构:
使用场景
原型模式不是万能的,但在某些场景下它确实是最优解。我根据自己的经验,总结了几个典型的使用场景:
| 场景 | 说明 | 我的经验 |
|---|---|---|
| 对象初始化成本高 | 构造函数需要加载文件、连接数据库等 | 游戏中的角色模板,加载一次后直接克隆 |
| 对象种类多且动态 | 运行时才能确定要创建什么类型 | 插件系统里,用原型注册表管理所有类型 |
| 避免工厂类层级爆炸 | 每个产品类对应一个工厂类,太冗余 | 用原型模式替代工厂方法,代码量减少一半 |
| 需要保存对象状态 | 快照、撤销操作等场景 | 编辑器里的撤销功能,用原型保存历史状态 |
核心要点:原型模式最适合「创建成本高、但复制成本低」的场景。如果你发现 new 一个对象要等好几秒,而拷贝只需要几毫秒,那原型模式就是你的菜。
我曾经在一个文档编辑器项目里用过原型模式。用户创建了一个复杂的表格,里面有各种格式、公式、样式。如果每次复制都要重新解析一遍,用户体验会非常差。我们用原型模式保存了一个「模板表格」,用户复制时直接克隆,速度提升了 10 倍以上。
嗯,最后提醒一句。原型模式虽然好用,但别滥用。如果你的对象很简单,new 一下就完事,那没必要搞这么复杂。设计模式是工具,不是目的。
避坑指南:我曾经在实现原型模式时,忘了处理对象内部的互斥锁、文件句柄等资源。这些资源不能简单拷贝,需要特殊处理。记得在 Clone() 方法里逐一检查每个成员变量。