1. 概述
本文将深入探讨创建型设计模式中的一种——原型模式(Prototype Pattern)。我们不会停留在理论层面,而是结合 Java 实际编码,手把手实现该模式的核心逻辑。
同时,也会分析它的适用场景、优势与踩坑点。对于有经验的开发者来说,这不仅仅是一个“复制对象”的技巧,而是一种在特定场景下提升性能和解耦的利器。
2. 原型模式详解
原型模式的核心思想非常简单粗暴:当你已经有一个对象实例(即原型)时,后续创建同类对象不再通过 new 关键字构造,而是直接“克隆”这个原型。
举个游戏开发中的常见场景:
假设你正在开发一个 2D 游戏,地图背景需要大量树木。如果每次渲染都 new 一棵树,不仅效率低,而且每棵树的初始化参数(如高度、质量)可能还都一样。
更好的做法是:
- 先创建一棵“模板树”(原型)
- 需要新树时,直接克隆这棵树
- 只需调整位置、颜色等个别属性即可
这样做的好处是:避免重复的构造逻辑,提升性能,还能动态调整原型状态。
✅ 核心价值:
- 隐藏对象创建细节
- 避免复杂的构造函数调用
- 支持运行时动态配置原型
3. UML 类图
从图中可以看出:
Prototype
是一个接口,声明了clone()
方法ConcretePrototype1
和ConcretePrototype2
实现了具体的克隆逻辑Client
不关心具体类型,只需调用clone()
即可获得新实例
这种设计完美体现了面向接口编程和多态性的优势。
4. Java 实现方式
Java 中实现原型模式有多种方式,最常见的是使用 Cloneable
接口 + clone()
方法。但要注意⚠️:
Cloneable
是一个标记接口,如果不实现它而直接调用clone()
,会抛出CloneNotSupportedException
4.1 关键决策:浅拷贝 vs 深拷贝
在实现克隆时,必须明确选择:
类型 | 适用场景 | 风险 |
---|---|---|
✅ 浅拷贝 | 对象只包含基本类型或不可变对象(如 String、Integer) | 引用字段共用,修改会影响所有克隆体 |
✅ 深拷贝 | 包含可变引用字段(如 List、自定义对象) | 实现复杂,可能需递归拷贝 |
推荐方案:
- 使用拷贝构造函数(Copy Constructor)
- 或通过序列化/反序列化实现深拷贝(适用于可序列化的类)
4.2 不依赖 Cloneable 的实现(推荐)
为了避免 clone()
的各种坑(比如 protected 访问限制、异常处理等),我们可以自己定义 copy()
方法。
先定义一个抽象基类:
public abstract class Tree {
private double mass;
private double height;
private Position position;
// 构造函数、getter/setter 省略...
public abstract Tree copy();
}
然后实现具体子类:
public class PlasticTree extends Tree {
public PlasticTree(double mass, double height) {
this.mass = mass;
this.height = height;
}
@Override
public Tree copy() {
PlasticTree plasticTreeClone = new PlasticTree(this.getMass(), this.getHeight());
plasticTreeClone.setPosition(this.getPosition());
return plasticTreeClone;
}
}
public class PineTree extends Tree {
public PineTree(double mass, double height) {
this.mass = mass;
this.height = height;
}
@Override
public Tree copy() {
PineTree pineTreeClone = new PineTree(this.getMass(), this.getHeight());
pineTreeClone.setPosition(this.getPosition());
return pineTreeClone;
}
}
✅ 这种方式的优势:
- 完全掌控拷贝逻辑
- 避免
Cloneable
的诡异行为 - 易于实现深拷贝
- 更符合现代 Java 编程习惯
5. 测试验证
5.1 单个对象克隆测试
public class TreePrototypesUnitTest {
@Test
public void givenAPlasticTreePrototypeWhenClonedThenCreateA_Clone() {
double mass = 10.0;
double height = 5.0;
Position position = new Position(0, 0);
Position otherPosition = new Position(10, 10);
PlasticTree plasticTree = new PlasticTree(mass, height);
plasticTree.setPosition(position);
PlasticTree anotherPlasticTree = (PlasticTree) plasticTree.copy();
anotherPlasticTree.setPosition(otherPosition);
assertEquals(position, plasticTree.getPosition());
assertEquals(otherPosition, anotherPlasticTree.getPosition());
}
}
测试结果表明:克隆后的对象是独立实例,修改位置不会影响原对象。
5.2 批量克隆测试(体现多态优势)
@Test
public void givenA_ListOfTreesWhenClonedThenCreateListOfClones() {
double mass = 10.0;
double height = 5.0;
Position position = new Position(0, 0);
PlasticTree plasticTree = new PlasticTree(mass, height);
plasticTree.setPosition(position);
PineTree pineTree = new PineTree(mass, height);
pineTree.setPosition(position);
List<Tree> trees = Arrays.asList(plasticTree, pineTree);
List<Tree> treeClones = trees.stream().map(Tree::copy).collect(toList());
Tree plasticTreeClone = treeClones.get(0);
Tree pineTreeClone = treeClones.get(1);
assertEquals(height, plasticTreeClone.getHeight());
assertEquals(position, plasticTreeClone.getPosition());
}
⚠️ 亮点:客户端代码完全不知道具体类型,通过多态统一调用 copy()
,实现了对扩展开放,对修改关闭。
6. 优缺点分析
✅ 优势
- 性能优化:避免重复初始化,尤其适合构造成本高的对象
- 解耦创建逻辑:客户端无需知道具体类名
- 动态配置:可在运行时修改原型状态,影响后续克隆结果
- 简化复杂对象创建:相比工厂模式,更适合“微调复制”场景
❌ 缺点
- 深拷贝实现复杂:涉及嵌套对象、循环引用时容易出错
- 难以处理 final 字段:某些情况下无法在克隆中重新赋值
- 过度设计风险:简单对象没必要用原型模式
- 状态一致性问题:若原型被意外修改,会影响所有未来克隆体
7. 总结
原型模式是一种实用且高效的创建型模式,特别适用于:
- 对象初始化开销大
- 对象状态组合有限
- 需要大量相似对象的场景(如游戏、GUI 组件)
虽然 Java 提供了 Cloneable
接口,但实际开发中更推荐自定义 copy()
方法,避免其固有的缺陷。
源码已托管至 GitHub:https://github.com/baeldung/design-patterns-creational
合理使用原型模式,能让你的代码更简洁、性能更高。但在使用前务必评估是否真的需要——毕竟,最简单的方案往往是最好的。