原型模式(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;
    }
}

原型模式的核心结构

下面这张图展示了原型模式的整体结构,我建议你仔细看看。

原型模式结构图 Client(客户端) Prototype(原型接口) + clone(): Prototype ConcretePrototype1 - field1: String - field2: List<Object> + clone(): Prototype(深拷贝) ConcretePrototype2 - fieldA: Map<K,V> - fieldB: CustomObject + clone(): Prototype(浅拷贝) 每个具体原型类实现 clone() 方法,决定使用深拷贝还是浅拷贝

原型模式的优缺点

优点 缺点
减少对象创建的开销,特别是复杂对象 深拷贝实现复杂,嵌套对象多时容易出错
运行时动态创建对象,不依赖构造函数 需要为每个类实现 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