编辑报价方案时,需要恢复前一次商品清单。若把可写 List 当成“快照”存进历史,调用方随后增加一项,旧快照也会变;若每次都无限制保存完整清单,历史又会无限增长。保存状态不等于保留一个别名,还需要决定恢复资格和内存边界。

状态由原对象保存,历史只管凭据

labs/36 的 Before.save() 直接返回内部 List,测试把新商品加进调用方输入后,“旧快照”也被修改。After 的内部清单经 List.copyOf 防御性复制;Snapshot 只有私有字段和私有构造器,不给调用方可写状态。History.save(editor) 保存由原对象创建的不透明快照、只保留最新两份;恢复前确认它属于当前编辑器且仍在这份历史中,否则拒绝。测试还断言修改原输入不会改变编辑器、已过期快照不能恢复且拒绝时当前清单不变。

1
2
3
4
Scenario.History history = new Scenario.History(2);
Scenario.Snapshot saved = history.save(editor);
editor.edit(List.of("paper", "pen"));
history.restore(editor, saved);

这里的 Memento 意图是让恢复旧状态无需把编辑器内部可写表示交给历史管理方;普通查询 items() 仍返回只读清单值,不能据此说所有数据都被隐藏。Alternative.copy(items) 为只需一次“返回上一版”的情形直接复制列表,更少对象和生命周期规则;不必为每份可复制数据设一个 Caretaker。最大保留 2 是历史内部上限,外部调用方若长期持有 Snapshot,其对应列表和用于验证归属的编辑器引用仍会占内存;测试没有测实际堆内存和垃圾回收。快照不持久化、没有跨进程恢复能力。

方案 外部修改原清单 过期快照与空间成本
Before 所谓快照一起变 别名导致历史不可靠
After 已存值不变 仅接受同编辑器的最近两份历史快照
Alternative 显式副本不变 由调用方自行决定保存多少副本
flowchart LR
    Caller[编辑调用方] --> History[History]
    History --> Editor[After]
    Editor --> Snapshot[Snapshot owner 和 items]
    History --> Bounded[有上限的 snapshots]
    Bounded --> Snapshot
    History -.校验归属与保留后恢复.-> Editor

验证与练习

在 examples/design-patterns/ 执行 ./mvnw -B -ntp -pl labs/36 -am test 与累计 ./mvnw -B -ntp verify,实际环境、输入、原始输出和退出码见 examples/design-patterns/evidence/36/RUN.md。负例是旧版别名;边界是上限 0 拒绝、过期/跨编辑器恢复失败。

  1. 新增“只允许恢复最近一份快照”,先写旧内容恢复合同和更早快照拒绝的新断言,再改历史上限并复跑别名隔离测试。
  2. 如果编辑器数据始终只有一个短字符串,删除 History 改用直接值复制;通过断言比较额外历史对象是否仍有需求。

参考资料

上一节:35 协作集中会形成大对象吗;下一节:37 新操作与新节点哪个更贵。