复制一个报价草稿后,业务员把副本中第一项数量从 2 改成 9;原草稿是否也变成 9?如果只是调用 clone(),表层对象虽不同,里面的清单和可写商品项仍可能共享。复制的合同必须回答“哪个状态可共享”,而不是只检查 original != copied。

表层复制不等于隔离

Before.copy() 使用 Java Object.clone(),测试断言克隆后顶层身份不同、首个 Item 却是同一个引用;修改副本里的 quantity,原本的数量也变成 9。这是本例的反例,不是对所有 clone 实现的断言。After.copy() 用显式拷贝构造逐个新建 Item,还对构造时调用方传入的列表做同样的复制:既隔离副本,也隔离原始输入。负数量抛异常,失败前不改已有副本状态。

1
2
3
Scenario.After original = new Scenario.After(List.of(new Scenario.Item(2)));
Scenario.After copied = original.copy();
copied.changeQuantity(0, 9);

Alternative.fromQuantities(2) 从明确的值重新构造对象,调用方已经有原始数据时可能比先找一个原型更直接。Prototype 的理由是新对象可以从现存配置或复杂模板衍生,并在预定的共享边界内复制;不应为了演示 clone 强迫所有类实现 Cloneable。只读不可变值可以安全共享,连接、锁、文件句柄、监听器等资源却不能盲目深拷贝。示例只含单线程内存 Item,不涉及循环对象图或资源生命周期。

设计 顶层身份 内层可写数量
Before 新对象 仍共享,同改动
After 新对象 新建每个 Item,修改互不影响
Alternative 直接根据数值重建 不借用待复制对象
flowchart LR
    Original[Before 原草稿] --> Shared[共享 Item]
    Clone[Before.clone 副本] --> Shared
    AfterOriginal[After 原草稿] --> Own[原 Item]
    AfterCopy[After.copy 副本] --> New[新 Item]

验证与练习

在 examples/design-patterns/ 运行 ./mvnw -B -ntp -pl labs/19 -am test 和累计 ./mvnw -B -ntp verify,实际结果和原始输出见 examples/design-patterns/evidence/19/RUN.md。旧数量、浅拷贝共享、显式复制隔离与负数拒绝均由断言核对;性能和内存占用 NOT_RUN。

  1. 新增“每个商品项有可修改的标签列表”,先为新旧副本的标签隔离写失败断言,再修改拷贝边界并复跑旧数量合同。
  2. 商品元数据改为只读 record 时,写断言证明安全共享同一个元数据引用;解释为何不能据此共享可写商品项。

参考资料

上一节:18 构造请求何时检查不变量;下一节:20 唯一实例属于哪个范围。