原型模式:复制对象,而不是重新造轮子

说实话,原型模式是我在实际项目中用得最频繁的创建型模式之一。为什么?因为很多时候,创建一个对象的成本实在太高了。高到让你心疼。

我记得有一次,我在做一个大型的图形编辑器。每个图形对象都需要加载大量的纹理、字体、样式配置。如果每次新建一个图形都从头加载一遍,用户早就把电脑砸了。这时候,原型模式就派上了用场——直接复制一个已有的对象,改改属性就完事。

什么是原型模式?

说白了,原型模式就是通过复制现有对象来创建新对象。你不需要知道对象内部是怎么构造的,只需要调用一个克隆方法。

它的核心思想是:用克隆代替构造

你想想看,有些对象的构造过程有多复杂?

  • 需要从数据库加载数据
  • 需要执行复杂的计算
  • 需要初始化大量的子对象
  • 需要建立网络连接

如果每次都从头来一遍,性能上扛不住。原型模式就是解决这个问题的。

浅拷贝 vs 深拷贝

嗯,这里要注意。原型模式的关键在于拷贝的方式。我见过不少新手在这上面栽跟头。

先看一个简单的例子:

class Document {
    String title;
    List<String> tags;
    Image image;
    
    // 浅拷贝
    public Document shallowClone() {
        Document doc = new Document();
        doc.title = this.title;
        doc.tags = this.tags;      // 同一个引用
        doc.image = this.image;    // 同一个引用
        return doc;
    }
    
    // 深拷贝
    public Document deepClone() {
        Document doc = new Document();
        doc.title = this.title;
        doc.tags = new ArrayList<>(this.tags);  // 新列表
        doc.image = this.image.clone();           // 新对象
        return doc;
    }
}

看出区别了吗?

特性 浅拷贝 深拷贝
基本类型 复制值 复制值
引用类型 复制引用(指向同一对象) 创建新对象
性能 慢(需要递归复制)
独立性 共享内部对象 完全独立
我曾经踩过的坑: 有一次我用浅拷贝复制了一个文档对象,然后修改了副本的 tags 列表。结果原文档的 tags 也被改了。排查了半天才发现是浅拷贝的问题。从那以后,只要涉及可变对象的复制,我默认都用深拷贝。

原型模式的结构

我习惯把原型模式的结构画成一张图,这样理解起来更直观:

原型模式结构图 Prototype(接口) + clone(): Prototype ConcretePrototypeA - field1: String + clone(): Prototype ConcretePrototypeB - field2: List<String> + clone(): Prototype Client 通过 clone() 创建新对象

这个结构其实很简单:

  • Prototype:声明克隆方法的接口
  • ConcretePrototype:实现克隆方法,处理深拷贝或浅拷贝
  • Client:通过调用 clone() 来创建新对象,而不是 new

什么时候用原型模式?

我个人总结了几个典型场景:

  1. 对象构造成本高:比如需要加载配置文件、建立数据库连接、初始化缓存
  2. 对象状态只有细微差别:比如文档模板,大部分内容相同,只有标题和日期不同
  3. 需要避免构造函数的约束:有些对象的构造函数参数太多,或者构造逻辑太复杂
  4. 运行时动态指定对象类型:你可以在运行时注册原型,然后动态克隆

实际项目中的例子:

我在做一个报表系统时,每个报表模板都包含:

  • 复杂的图表配置(颜色、字体、布局)
  • 数据源连接信息
  • 权限控制列表
  • 导出格式设置

如果每次新建报表都从头配置,用户会疯掉。所以我们用原型模式,用户选择一个模板,克隆一份,然后修改标题和数据范围就行了。效率提升了至少 10 倍。

Java 中的 Cloneable 接口

Java 自带了 Cloneable 接口和 clone() 方法。但我得说一句,这个设计其实有点坑。

public class User implements Cloneable {
    private String name;
    private List<String> roles;
    
    @Override
    public User clone() {
        try {
            // 默认是浅拷贝
            return (User) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
    
    // 如果需要深拷贝,得自己实现
    public User deepClone() {
        User cloned = this.clone();
        cloned.roles = new ArrayList<>(this.roles);
        return cloned;
    }
}
我的建议: 不要直接依赖 Java 的 Cloneable。它有几个问题:
1. clone() 方法返回的是 Object,需要强转
2. 默认是浅拷贝,容易出 bug
3. 如果父类没有正确实现,会抛异常

我更喜欢自己定义一个 Prototype 接口,明确区分 shallowClone() 和 deepClone()。

深拷贝的几种实现方式

深拷贝说起来简单,做起来其实有不少门道。我整理了几种常见做法:

方式 实现 适用场景
手动复制 逐个字段 new 新对象 对象结构简单,字段少
序列化 对象 -> 字节流 -> 新对象 对象层级深,但所有类都需实现 Serializable
JSON 转换 对象 -> JSON -> 新对象 需要跨语言复制,或对象结构动态变化
克隆库 使用 Apache Commons Lang 的 SerializationUtils 不想自己写序列化逻辑
// 序列化方式实现深拷贝
public class DeepCloneUtil {
    @SuppressWarnings("unchecked")
    public static <T extends Serializable> T deepClone(T obj) {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(obj);
            
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            return (T) ois.readObject();
        } catch (Exception e) {
            throw new RuntimeException("深拷贝失败", e);
        }
    }
}
注意: 序列化方式虽然方便,但性能开销大。如果对象很大,或者需要频繁克隆,建议用手动复制或者专门的克隆库。我在一个高并发场景下用过序列化克隆,结果 CPU 直接飙到 90%,后来改成手动复制才降下来。

原型模式 vs 工厂模式

有同学会问:原型模式和工厂模式有什么区别?

我简单说一下:

  • 工厂模式:每次都是 new 一个新对象,构造逻辑在工厂里
  • 原型模式:复制一个已有对象,构造逻辑在 clone() 方法里

说白了,工厂模式是「从零开始造」,原型模式是「复制粘贴改一改」。如果你的对象构造很复杂,或者你需要保留对象的部分状态,原型模式更合适。

总结

原型模式不是什么高深的技术,但它解决了一个很实际的问题:如何高效地创建复杂对象

记住几个要点:

  • 浅拷贝快但不安全,深拷贝安全但慢
  • 根据场景选择合适的深拷贝方式
  • 不要盲目依赖语言自带的克隆机制
  • 原型模式特别适合模板、配置、文档这类场景

我在项目中用原型模式最多的就是做「模板克隆」和「快照恢复」。这两个场景几乎每天都会遇到。你想想看,如果没有原型模式,每次都要重新加载配置、重新初始化对象,那得浪费多少时间?

好了,原型模式就聊到这里。记住一句话:能复制就别重新造


公众号:蓝海资料掘金营,微信deep3321