Skip to content

约 2 分钟阅读

原型模式 ​

原型模式通过复制已有对象创建新对象,适合“保留大部分配置,只改少数字段”的需求。它不要求实现 Java Cloneable;显式 copy()、复制构造器或 copyWith() 都可以表达复制合同。

导出模板复制时的共享与独立字段

复制的是配置还是身份 ​

用户复制一份订单导出模板时,希望沿用列顺序与文件格式,但新模板应有自己的名称与标识。原模板的列配置不能因副本编辑而改变;不可变的列名字符串可以共享。运行中的任务、数据库主键、幂等键、连接句柄和锁则不能盲目复制。

因此,复制前先定义边界:哪些是独立的可变状态,哪些是可共享的不可变值,哪些字段需要重新生成。复制对象是否比重新构建更快,需要结合对象图和初始化成本测量,不能从“克隆”这个名称得出结论。

浅拷贝与深拷贝 ​

Java Object.clone() 按字段复制对象,引用字段仍指向原来的对象,是浅拷贝。给外层对象加一个 Cloneable 并不会递归复制其列表或列表元素。深拷贝也应以业务需要的可变子图为边界,不需要复制不可变字符串,更不应复制外部连接。

字段模板复制合同原因
名称用调用者提供的新名称副本身份应可区分
列表容器新建列表编辑副本的列不影响原模板
列名字符串可以共享String 不可变
可变列设置对象按字段继续复制仅复制容器仍会共享元素
数据库主键、任务状态新建或不带入配置复制不等于复制业务执行实例

可运行示例 ​

以下 Java 17+ 示例中列名是不可变字符串,所以新建列表就满足当前复制边界。

java
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

public final class PrototypeDemo {
    static final class ExportTemplate {
        private final String name;
        private final List<String> columns;

        ExportTemplate(String name, List<String> columns) {
            this.name = Objects.requireNonNull(name);
            this.columns = new ArrayList<>(List.copyOf(columns));
        }

        ExportTemplate copyAs(String newName) {
            return new ExportTemplate(newName, columns);
        }

        void addColumn(String column) {
            columns.add(Objects.requireNonNull(column));
        }

        List<String> columns() {
            return List.copyOf(columns);
        }

        String name() {
            return name;
        }
    }

    public static void main(String[] args) {
        ExportTemplate original = new ExportTemplate(
                "订单基础", List.of("订单号", "金额"));
        ExportTemplate copy = original.copyAs("订单物流");
        copy.addColumn("物流单号");

        if (original == copy || original.columns().size() != 2
                || copy.columns().size() != 3) {
            throw new AssertionError("复制边界错误");
        }
        System.out.println(original.name() + ": " + original.columns());
        System.out.println(copy.name() + ": " + copy.columns());
    }
}

使用 javac PrototypeDemo.java && java PrototypeDemo 运行。原模板仍有两列,副本有三列。构造器复制传入集合,查询方法返回只读快照,同时收敛了输入与输出两端的可变引用泄漏。

为什么不直接序列化复制 ​

序列化往返可以复制部分对象图,但要求类型支持相应机制,并涉及类型信息、版本、性能和数据安全。它不是所有对象都适用的通用复制器;不能为复制方便而反序列化不可信数据。

显式复制能看清每个字段的处理,代价是新增字段时必须维护合同。循环引用、共享子对象和多态对象尤其需要注意:简单递归可能无限循环,也可能把本应共享的同一子对象复制多份。

验收与适用边界 ​

先修改副本的每个可变字段,验证原模板不变,再反向修改原模板。若存在嵌套对象,需要覆盖元素级修改;只比较两个顶层引用不同并不够。还应检查副本主键、幂等键和执行状态没有沿用。

该示例面向单线程模板编辑。若原对象会并发变化,复制时必须使用一致快照或同步机制,否则可能读到不同时间的字段组合。若对象很简单或不需要复用已有配置,直接构造或建造者更直观。

别急,先让缓存热一下。