原型模式(Prototype):工作流引擎中的流程模板复制
说实话,设计模式里有些模式看起来很简单,但用好了真能解决大问题。原型模式就是其中之一。
它的定义是:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。说白了,就是先有个现成的对象,然后直接复制一份,而不是从头 new 一个。
我最早接触这个模式,是在做 OA 系统的审批流程模块时。那时候有个需求——用户想复制一个已有的审批流程模板,改几个节点就能用。如果每次都重新建,那体验太差了。
为什么需要原型模式?
你想想看,有些对象的创建成本很高。比如一个复杂的游戏角色,它身上挂了几十件装备、十几个技能、还有各种属性 buff。如果每次创建都去数据库里查一遍、组装一遍,性能上扛不住。
再比如工作流引擎里的流程模板。一个模板可能有几十个审批节点、条件分支、会签设置。每次新建都从头配置,用户会疯掉的。
原型模式的核心价值就在这里:通过拷贝已有对象来创建新对象,避免重复的初始化开销。
适用场景速查
- 对象创建成本高(数据库查询、复杂计算、网络 IO)
- 对象之间差异很小,只是少数属性不同
- 需要避免构造函数中的复杂逻辑
- 运行时动态指定对象的类型
深拷贝 vs 浅拷贝——这个坑我踩过
原型模式最核心的问题就是拷贝方式。我记得有一次,我在做游戏角色的复制功能时,用了 Java 默认的 clone() 方法。结果复制出来的角色和原角色共享了同一份装备列表。改了一个角色的装备,另一个也跟着变了。
嗯,这就是典型的浅拷贝问题。
咱们用代码来说清楚这件事。
// 浅拷贝——危险!
public class GameCharacter implements Cloneable {
private String name;
private List<Equipment> equipmentList; // 引用类型
private Map<String, Skill> skillMap; // 引用类型
@Override
protected GameCharacter clone() throws CloneNotSupportedException {
return (GameCharacter) super.clone(); // 只拷贝了引用
}
}
// 深拷贝——正确做法
public class GameCharacter implements Cloneable {
private String name;
private List<Equipment> equipmentList;
private Map<String, Skill> skillMap;
@Override
protected GameCharacter clone() {
GameCharacter copy = new GameCharacter();
copy.name = this.name;
// 深拷贝:创建新的集合,并逐个拷贝元素
copy.equipmentList = new ArrayList<>();
for (Equipment eq : this.equipmentList) {
copy.equipmentList.add(eq.clone()); // Equipment 也要实现 Cloneable
}
copy.skillMap = new HashMap<>();
for (Map.Entry<String, Skill> entry : this.skillMap.entrySet()) {
copy.skillMap.put(entry.getKey(), entry.getValue().clone());
}
return copy;
}
}
我曾经踩过的坑: 有一次在 OA 系统里,我用了浅拷贝复制流程模板。结果用户修改了复制后的模板的审批人,原模板的审批人也跟着变了。排查了半天才发现是浅拷贝的问题。从那以后,凡是涉及引用类型的属性,我必做深拷贝。
工作流引擎中的实战案例
咱们来看一个完整的例子。假设我们在做一个 OA 系统的审批流程模块。
// 流程模板——原型
public class ApprovalTemplate implements Cloneable {
private String templateName;
private List<ApprovalNode> nodes; // 审批节点列表
private Map<String, Rule> rules; // 规则配置
private int version;
public ApprovalTemplate(String name) {
this.templateName = name;
this.nodes = new ArrayList<>();
this.rules = new HashMap<>();
this.version = 1;
}
// 添加审批节点
public void addNode(ApprovalNode node) {
nodes.add(node);
}
// 深拷贝
@Override
public ApprovalTemplate clone() {
ApprovalTemplate copy = new ApprovalTemplate(this.templateName + "(副本)");
copy.version = this.version + 1;
// 深拷贝节点列表
for (ApprovalNode node : this.nodes) {
copy.nodes.add(node.clone());
}
// 深拷贝规则
for (Map.Entry<String, Rule> entry : this.rules.entrySet()) {
copy.rules.put(entry.getKey(), entry.getValue().clone());
}
return copy;
}
// 业务方法:修改某个节点的审批人
public void updateApprover(int nodeIndex, String newApprover) {
if (nodeIndex < nodes.size()) {
nodes.get(nodeIndex).setApprover(newApprover);
}
}
}
// 使用示例
public class WorkflowEngine {
private Map<String, ApprovalTemplate> templateCache = new HashMap<>();
public ApprovalTemplate createFromTemplate(String templateId) {
ApprovalTemplate source = templateCache.get(templateId);
if (source == null) {
// 从数据库加载
source = loadFromDatabase(templateId);
templateCache.put(templateId, source);
}
return source.clone(); // 返回副本,不影响原模板
}
}
我的个人习惯: 在实现 Cloneable 接口时,我会在 clone() 方法上加 @Override 注解,并且显式声明返回类型为当前类。这样调用方不需要强转,代码更干净。
游戏开发中的角色复制
游戏场景下,原型模式的应用更直观。比如一个 BOSS 角色,它有固定的装备模板和技能组合。每次生成新 BOSS 时,直接复制原型,然后随机调整几个属性值就行。
// 游戏角色原型
public class GameRole implements Cloneable {
private String roleId;
private String roleName;
private int level;
private int hp;
private int mp;
private List<Equipment> equipments;
private List<Skill> skills;
private Map<String, Integer> attributes; // 属性:力量、敏捷、智力等
@Override
public GameRole clone() {
GameRole copy = new GameRole();
copy.roleId = UUID.randomUUID().toString(); // 新 ID
copy.roleName = this.roleName;
copy.level = this.level;
copy.hp = this.hp;
copy.mp = this.mp;
// 深拷贝装备
copy.equipments = new ArrayList<>();
for (Equipment eq : this.equipments) {
copy.equipments.add(eq.clone());
}
// 深拷贝技能
copy.skills = new ArrayList<>();
for (Skill sk : this.skills) {
copy.skills.add(sk.clone());
}
// 深拷贝属性
copy.attributes = new HashMap<>(this.attributes);
return copy;
}
}
原型模式的核心结构
下面这张图展示了原型模式的整体结构,我建议你仔细看看。
原型模式的优缺点
| 优点 | 缺点 |
|---|---|
| 减少对象创建的开销,特别是复杂对象 | 深拷贝实现复杂,嵌套对象多时容易出错 |
| 运行时动态创建对象,不依赖构造函数 | 需要为每个类实现 clone() 方法 |
| 可以保存对象的状态快照 | 循环引用问题需要特殊处理 |
| 比 new 关键字更灵活,支持多态克隆 | 如果类有 final 字段,clone() 可能受限 |
实现要点与避坑指南
我个人总结了几个关键点,分享给你。
- 明确拷贝策略: 每个引用类型字段都要问自己——这个字段需要深拷贝还是浅拷贝?不可变对象(如 String、Integer)可以浅拷贝,可变对象必须深拷贝。
- 使用序列化实现深拷贝: 如果对象层级很深,手动写 clone() 很痛苦。可以用序列化/反序列化来实现深拷贝,但要注意性能开销。
- 注意单例对象: 如果原型对象引用了单例,拷贝时应该保持引用,而不是创建新实例。
- 缓存原型对象: 我习惯用一个原型注册表(PrototypeRegistry)来管理所有原型,避免重复创建。
我曾经踩过的坑: 有一次在游戏项目中,角色的技能列表里有一个技能是「被动光环」,它引用了周围的其他角色。我用序列化做了深拷贝,结果复制出来的角色和原角色互相引用,形成了死循环。最后不得不手动处理这种特殊引用关系。
原型注册表模式
在实际项目中,我通常会配合注册表一起使用。这样管理原型更方便。
// 原型注册表
public class PrototypeRegistry {
private Map<String, ApprovalTemplate> templates = new HashMap<>();
public void register(String key, ApprovalTemplate template) {
templates.put(key, template);
}
public ApprovalTemplate createCopy(String key) {
ApprovalTemplate prototype = templates.get(key);
if (prototype == null) {
throw new IllegalArgumentException("未找到原型:" + key);
}
return prototype.clone();
}
// 批量预加载
public void loadFromDatabase() {
// 从数据库加载所有模板,注册到缓存中
List<ApprovalTemplate> allTemplates = dao.findAll();
for (ApprovalTemplate t : allTemplates) {
register(t.getTemplateId(), t);
}
}
}
我的建议: 原型注册表最好在系统启动时初始化,把常用的原型都加载到内存中。这样运行时复制操作就是纯内存操作,性能极好。
总结
原型模式说白了就是「复制粘贴」的思想。它不复杂,但用好了能大幅提升系统性能和用户体验。
记住三个关键点:
- 深拷贝 vs 浅拷贝——根据业务场景选择
- 原型注册表——集中管理原型对象
- clone() 方法的实现——注意引用类型的处理
我在多个项目中都用过这个模式,从 OA 系统到游戏服务器,从配置管理到数据快照。每次用都觉得——嗯,设计模式这东西,真不是白学的。
公众号:蓝海资料掘金营,微信deep3321