1. 原型模式到底解决什么问题,别和工厂模式搞混了
如果你正在写一个系统,需要创建大量相似但又不完全相同的对象,比如游戏里成千上万个属性略有差异的怪物,或者文档编辑器里复制一个包含复杂格式的图形,你会怎么写?最直接的想法可能是
new
一个对象,然后挨个设置属性。但这样做有两个大问题:一是性能差,每次都要走一遍完整的构造和初始化流程;二是代码耦合度高,客户端必须知道对象内部的所有构造细节。
原型模式(Prototype Pattern)就是为了解决这个“高效创建相似对象”的问题。它的核心思想非常简单:
通过复制一个现有实例(原型)来创建新对象,而不是通过
new
关键字调用构造函数
。这就像生物学的“克隆”,你有一个细胞,通过分裂就能得到一个一模一样的新细胞。
很多人容易把它和工厂模式搞混。简单区分一下:
- 工厂模式 :关注的是“创建哪种产品”。你告诉工厂“我要一把斧头”,工厂给你生产一把新的斧头。重点是隐藏具体产品的构造逻辑。
- 原型模式 :关注的是“如何高效地复制一个产品”。你手头已经有一把精心打造、附了魔的传奇斧头,现在你需要一百把一模一样的去武装你的士兵。重点是“复制”这个动作本身的高效和便捷。
所以,原型模式最关键的场景就两个:
1. 创建成本高
(比如对象初始化需要从数据库、网络加载大量数据);
2. 系统需要动态指定创建的对象类型
(运行时才知道要创建哪个具体类的对象)。在 Spring 框架中,Bean 的作用域为
prototype
时,每次获取都会返回一个新的实例,其底层思想就与原型模式一脉相承,虽然 Spring 的实现更复杂。
2. 理解原型模式的结构:接口、实现与深浅拷贝
原型模式的结构非常清晰,通常包含以下角色:
-
原型接口 (Prototype Interface)
:声明一个克隆自身的方法,通常是
clone()或copy()。这是所有具体原型类的契约。 - 具体原型类 (Concrete Prototype) :实现原型接口,提供克隆自身的具体实现。这是模式的核心。
- 客户端 (Client) :通过调用原型对象的克隆方法来创建新对象,而无需知道其具体类。
用代码来理解是最直接的。我们来看一个最简单的 Java 示例:
// 1. 原型接口
interface Prototype extends Cloneable {
Prototype clone();
}
// 2. 具体原型类
class ConcretePrototype implements Prototype {
private String field;
private List<String> listField;
public ConcretePrototype(String field, List<String> listField) {
this.field = field;
this.listField = listField;
}
// 实现克隆方法
@Override
public Prototype clone() {
try {
// 这里调用的是Object的clone(),是浅拷贝
ConcretePrototype copy = (ConcretePrototype) super.clone();
// 对于引用类型,如果需要深拷贝,必须手动处理
// copy.listField = new ArrayList<>(this.listField);
return copy;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
// getters and setters...
}
// 3. 客户端使用
public class Client {
public static void main(String[] args) {
ConcretePrototype original = new ConcretePrototype("原始数据", new ArrayList<>(Arrays.asList("A", "B")));
ConcretePrototype copy = (ConcretePrototype) original.clone();
System.out.println(original == copy); // false,是两个不同的对象
System.out.println(original.getListField() == copy.getListField()); // true!浅拷贝,共享同一个List引用
}
}
这段代码引出了原型模式实现中最关键、也最容易出错的概念: 浅拷贝 (Shallow Copy) 与深拷贝 (Deep Copy) 。
-
浅拷贝
:
Object.clone()的默认行为。它复制对象的所有基本数据类型字段,但对于对象引用字段,只复制引用地址,新旧对象共享同一个子对象。就像上例中的listField,修改copy的列表,original的列表也会跟着变。这在很多场景下是灾难性的。 -
深拷贝
:不仅复制对象本身,还递归复制其所有引用字段指向的对象,生成一个完全独立的副本。要实现深拷贝,通常需要在
clone()方法中手动为新对象的每个引用字段创建新的实例。
对于上面的
ConcretePrototype
,一个简单的深拷贝实现如下:
@Override
public Prototype clone() {
try {
ConcretePrototype copy = (ConcretePrototype) super.clone();
// 深拷贝关键:为引用字段创建新的对象
copy.listField = new ArrayList<>(this.listField); // 创建新的ArrayList,复制元素
return copy;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
现在,
original.getListField() == copy.getListField()
将返回
false
。在实际项目中,如果原型对象内部结构非常复杂(嵌套多层对象、集合等),实现一个正确、高效的深拷贝可能非常棘手,有时需要借助序列化/反序列化(如 Java 的
Serializable
)或第三方库(如 Apache Commons Lang 的
SerializationUtils.clone
)。
3. 在不同语言中的实现与“坑点”
原型模式的思想是通用的,但不同编程语言对其的支持和实现方式差异很大。
3.1 Java 的实现
Java 通过
Cloneable
标记接口和
Object.clone()
本地方法提供了原生支持,但设计上饱受诟病。
-
Cloneable是个空接口 ,它只是一个标记,不包含任何方法。如果一个类没有实现Cloneable却调用了clone(),会抛出CloneNotSupportedException。 -
Object.clone()是 protected 方法 ,你必须在子类中重写并提升为 public。 - 默认是浅拷贝 ,深拷贝需要开发者自己负责。
-
不调用构造函数
,
clone()直接复制内存块。这意味着如果对象构造过程中有副作用(如注册监听器、初始化外部资源),这些逻辑在克隆时不会执行。
因此,在 Java 中,很多专家建议
不要使用
Cloneable/clone()
,而是实现一个自定义的拷贝构造函数或拷贝工厂方法,这样更清晰、更可控。
// 更推荐的拷贝构造函数方式
class ConcretePrototype {
private String field;
private List<String> listField;
public ConcretePrototype(ConcretePrototype other) {
this.field = other.field;
this.listField = new ArrayList<>(other.listField); // 深拷贝
}
}
3.2 C++ 的实现
C++ 没有内置的克隆机制,但可以通过拷贝构造函数和拷贝赋值运算符来实现原型模式。这也是实现深拷贝的标准场所。
-
拷贝构造函数
:
ClassName(const ClassName& other) -
拷贝赋值运算符
:
ClassName& operator=(const ClassName& other)
在 C++ 中,你必须非常小心“ 三法则 ”(如果定义了析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个,通常都需要定义全部三个),以及在 C++11 之后的“ 五法则 ”(加上移动构造函数和移动赋值运算符)。忘记实现深拷贝会导致指针重复释放等严重内存错误。
3.3 Python 的实现
Python 中实现原型模式非常灵活,主要使用
copy
模块。
-
copy.copy(x):浅拷贝。创建一个新的复合对象,然后(尽可能)将原始对象中找到的对象的 引用 插入其中。 -
copy.deepcopy(x):深拷贝。创建一个新的复合对象,然后递归地将原始对象中找到的对象的 副本 插入其中。
你可以通过实现
__copy__()
和
__deepcopy__()
特殊方法来定制拷贝行为。
import copy
class Prototype:
def __init__(self, value, list_value):
self.value = value
self.list_value = list_value
def __copy__(self):
# 自定义浅拷贝
new_one = type(self)(self.value, self.list_value) # 注意,这里list_value是引用传递
return new_one
def __deepcopy__(self, memo):
# 自定义深拷贝
import copy
new_one = type(self)(self.value, copy.deepcopy(self.list_value, memo))
return new_one
# 使用
obj = Prototype(1, [1, 2, 3])
shallow_copy = copy.copy(obj)
deep_copy = copy.deepcopy(obj)
3.4 JavaScript 的实现
在 JavaScript 中,对象复制是日常操作,但同样存在深浅拷贝问题。
-
浅拷贝
:
Object.assign({}, obj)、扩展运算符{...obj}、Array.prototype.slice()。 -
深拷贝
:
JSON.parse(JSON.stringify(obj))(有局限,不能处理函数、undefined、循环引用等)、structuredClone()(现代浏览器和 Node.js 支持,能处理更多类型和循环引用)、使用 Lodash 的_.cloneDeep。
对于需要原型模式的自定义类,可以实现一个
clone
方法。
class GameUnit {
constructor(name, stats) {
this.name = name;
this.stats = { ...stats }; // 避免外部修改影响内部
}
clone() {
// 实现深拷贝
const clonedStats = JSON.parse(JSON.stringify(this.stats));
return new GameUnit(this.name, clonedStats);
}
}
一个通用坑点
:无论哪种语言,当你的原型对象包含
文件句柄、网络连接、数据库连接、线程
等不可复制或唯一性的资源时,克隆行为必须被慎重处理,通常需要重新初始化或设置为
null
,否则会导致资源冲突或状态混乱。
4. 实战场景:何时用,怎么用,如何管理原型
理解了原理和实现,我们来看看原型模式在哪些真实场景下能发挥价值。
4.1 典型使用场景
- 游戏开发 :这是教科书级的例子。一个“兽人步兵”原型对象,定义了基础属性(生命值、攻击力、模型路径)。当需要生成一队兽人步兵时,只需克隆这个原型,然后微调每个实例的个别属性(如位置、ID)。这比每次从零构造一个兽人要高效得多。
- 图形编辑器 :用户绘制了一个复杂的组合图形(如一个流程图节点,包含文字、边框、连接点)。复制粘贴操作本质上就是克隆这个图形原型,生成一个位置不同但外观和结构完全相同的新图形。
- 配置对象 :系统有一个复杂的默认配置对象,包含数百个参数。不同模块或用户可能需要基于这个默认配置进行少量修改。克隆默认配置作为起点,比重新构建一个配置对象并逐一设置所有默认值要方便和安全。
- 减少数据库查询 :某个对象初始化需要执行多次昂贵的数据库查询来填充数据。如果这个对象需要被多次使用(且数据相对静态),可以在第一次加载后将其缓存为原型,后续需求通过克隆原型来获得对象,避免重复查询。
- 动态加载类 :在某些框架中,你可以在运行时动态注册新的原型类。客户端只需要知道原型接口,就可以通过一个标识符(如字符串)从原型管理器获取对应的原型并进行克隆,无需在代码中硬编码具体的类名。这提高了系统的扩展性。
4.2 原型管理器 (Prototype Manager)
当系统中存在多种原型时,直接让客户端记住并管理所有原型对象是不现实的。这时可以引入一个
原型管理器
(或原型注册表),它通常是一个简单的键值对集合(如
Map
)。
public class PrototypeManager {
private static Map<String, Prototype> prototypeMap = new HashMap<>();
static {
// 初始化时注册一些常用原型
prototypeMap.put("Soldier", new SoldierUnit(100, 10));
prototypeMap.put("Tank", new TankUnit(500, 50));
}
public static void registerPrototype(String key, Prototype prototype) {
prototypeMap.put(key, prototype);
}
public static Prototype getClone(String key) {
Prototype prototype = prototypeMap.get(key);
if (prototype != null) {
return prototype.clone(); // 关键:返回的是克隆体,不是原型本身!
}
return null;
}
}
// 客户端使用
Unit myArmy = (Unit) PrototypeManager.getClone("Soldier");
使用管理器时的一个关键细节
:
getClone
方法返回的一定是克隆体。如果你错误地返回了原型本身,那么所有客户端拿到的都是同一个对象,修改其中一个会影响到所有其他“克隆体”,这完全违背了原型模式的初衷。
4.3 与其它创建型模式的对比与选择
在项目中选择设计模式时,清晰地区分它们能帮你做出更好决策。
| 模式 | 核心目的 | 适用场景 |
|---|---|---|
| 工厂方法 | 将对象 创建过程 延迟到子类,解决“单个对象”的创建问题。 | 创建逻辑复杂,需要根据不同条件创建不同产品,且产品有统一接口。 |
| 抽象工厂 | 创建 一系列相关或依赖 的对象族,而不指定具体类。 | 系统需要多个产品族,且保证族内产品兼容。 |
| 建造者 | 将复杂对象的 构建过程 与表示分离,允许逐步构造。 | 对象构造参数多,且有些参数可选,希望构造过程清晰、灵活。 |
| 原型 | 通过 复制现有实例 来创建新对象,避免昂贵的初始化开销。 |
1. 创建成本高(资源、时间)。
2. 系统需独立于其产品的创建、组合和表示。 |
简单来说:
- 需要 灵活控制创建过程 或 构建复杂对象 -> 考虑工厂或建造者。
- 需要 高效复制已有对象 或 运行时动态指定类型 -> 考虑原型。
5. 在 Spring、Qt 等框架中的体现与自实现要点
很多成熟框架都内置或体现了原型模式的思想,理解它们有助于我们更好地使用框架。
5.1 Spring Framework 中的原型作用域
在 Spring 中,当你将一个 Bean 的作用域定义为
prototype
时,每次从容器中请求该 Bean(通过
getBean()
或注入),Spring 都会返回一个
全新的实例
。这可以看作是原型模式的一种应用:Spring 容器缓存了 Bean 的定义(原型),每次根据这个定义“克隆”出一个新实例。
需要注意的是,Spring 的
prototype
Bean 的“克隆”过程,实际上是重新执行了一遍 Bean 的初始化生命周期(调用构造函数、注入依赖、执行初始化方法等),并非 Java 默认的
Object.clone()
。这更接近于我们之前提到的“拷贝构造函数”方式。
5.2 Qt/C++ 中的原型模式应用
在 Qt 这样的 GUI 框架中,原型模式常用于图形项(
QGraphicsItem
)的复制。Qt 的图形视图框架提供了
QGraphicsItem::clone()
虚函数,具体的图形项子类(如自定义的图表节点)可以重写此函数来实现复制功能。当用户在界面上复制一个图形项时,框架就是调用这个克隆方法。
在《Qt C++设计模式实战指南》这类资料中,原型模式常被用于实现诸如“工具栏模板”、“图形元件库”等功能。你可以从元件库中拖拽一个“电阻”原型到电路图中,实际上就是克隆了一个新的电阻实例。
5.3 自己实现时的核心要点与检查清单
如果你要在自己的项目中应用原型模式,我建议按以下步骤和要点进行:
第一步:评估是否真需要
- [ ] 创建对象的过程是否真的非常昂贵(IO、计算、网络)?
- [ ] 是否需要创建大量相似对象?
- [ ] 对象的类型是否需要运行时动态决定?
-
如果以上都不是,直接用
new或工厂可能更简单。
第二步:设计原型接口与类
-
[ ] 定义一个清晰的克隆方法,如
Prototype clone()或Prototype copy()。 - [ ] 让具体原型类实现该接口。
- [ ] 立即决定采用浅拷贝还是深拷贝 。对于包含可变引用字段的类, 默认优先考虑深拷贝 ,除非有明确的共享需求。
第三步:实现克隆方法
-
Java
:考虑使用
拷贝构造函数
或
静态工厂方法
代替
Cloneable。如果必须用clone(),务必处理好所有引用字段。 - C++ :正确实现 拷贝构造函数 和 拷贝赋值运算符 ,遵循三/五法则。
-
Python
:实现
__copy__和__deepcopy__。 -
JavaScript
:实现
clone()方法,内部使用深拷贝逻辑(如structuredClone或递归复制)。 -
[ ] 在
clone方法中, 避免调用任何可能改变原型对象状态的操作 。
第四步:处理复杂依赖与资源
- [ ] 如果原型对象持有数据库连接、文件句柄等资源,在克隆时应该如何处理?(通常是置空或创建新连接)
-
[ ] 如果对象图存在循环引用,你的深拷贝算法能否处理?(
JSON.stringify不能,需要递归算法加备忘录) -
[ ] 考虑使用
序列化/反序列化
作为实现深拷贝的通用方案(如 Java 的
Serializable),但要关注性能。
第五步:考虑引入管理器
-
[ ] 如果原型种类多,添加一个
PrototypeManager来集中注册和获取。 - [ ] 确保管理器返回的是克隆体,而不是原型本身。
第六步:测试
- [ ] 测试克隆出的对象与原型对象 相等(equals)但不相同(==) 。
- [ ] 测试修改克隆对象的字段 不会影响 原型对象(深拷贝测试)。
- [ ] 测试包含集合、嵌套对象等复杂结构的克隆是否正确。
- [ ] 性能测试:对比克隆和新建对象的开销,验证使用模式的收益。
最后,记住原型模式是一种“以空间换时间”的模式。它通过预先创建并缓存原型对象,牺牲了一些内存,换来了运行时对象创建的速度。在对象创建成本高昂的场景下,这笔交易非常划算。但在对象结构非常简单,或者每个对象都差异巨大的场景下,它可能反而增加了复杂度。

2368

被折叠的 条评论
为什么被折叠?



