我们平时写业务代码,经常遇到"用户点了撤销,数据怎么还原"这种需求。最直觉的做法,就是在操作前把对象当场存个副本塞进列表里,等要回退的时候再取出来覆盖回去。这个做法本身没问题,但真正落地的时候,各种细节问题就全冒出来了:对象是引用类型,直接塞进列表的只是地址,等你要用的时候,数据早被改得面目全非了;如果你用序列化做深拷贝,又会遇到字段增删导致反序列化失败、性能开销大、循环引用直接栈溢出这些情况。备忘录模式(Memento Pattern)就是专门解决这一类"对象状态保存与恢复"问题的设计模式,它把状态快照的职责从业务对象里剥离出来,让保存、恢复这些操作不再和业务逻辑纠缠在一起。
这篇文章我打算结合自己项目里的实战经历,把备忘录模式从原理到落地一次讲透,包括它到底解决什么痛点、三个核心角色怎么设计、实现的时候有哪些绕不开的深坑,以及和命令模式、序列化这些常见方案的对比。不管你是刚接触设计模式的初学者,还是已经在项目里写过不少业务代码的开发者,看完这篇应该都能直接拿来用。
1. 一个看似简单却翻过车的需求:订单编辑的恢复功能
我先说一个真实的踩坑经历。去年做一个后台管理系统,运营同学编辑订单的时候手一抖把金额填错了,填错也就算了,关键是那个表单没有保存前校验,直接提交了。主管当场脸黑,说必须加一个"撤销到上次保存状态"的功能,最好能多点几步,支持撤销多次。
我第一反应是,这不简单吗?我在提交前偷偷把整个订单对象存一份,放进一个 List,用户点撤销的时候 pop 出来塞回去就行了。于是稀里哗啦写完了:
java复制public class Order {
private Long id;
private String customerName;
private BigDecimal amount;
private Integer status;
// getter/setter 省略
}
操作的时候直接:
java复制history.add(order);
order.setAmount(new BigDecimal("9999"));
然后测试的时候就露馅了,而且露馅方式特别尴尬。我改了 order 的 amount,回头一打印 history 里存的所有对象,发现它们的 amount 全变成了 9999。为什么?因为 Java 对象是引用类型,history.add(order) 存进去的是同一个对象的地址,不是拷贝。你后面一改,之前存的那些"历史状态"也跟着变了,等于撤销功能形同虚设。
后来我换了序列化深拷贝的方案,把订单对象转成 JSON 字符串或者字节流存起来,以后恢复的时候再反序列化回来。这招初期确实管用,但问题接踵而至。订单对象里嵌套的商品快照是个包含泛型集合的复杂结构,反序列化的时候遇到类型擦除,集合里的子类对象全被还原成了父类类型,字段直接丢失;还有一次更狠,我在商品类里加了一个双向关联的引用,序列化的时候直接栈溢出。那段时间我真的是被这个"简单的功能"折磨得够呛。
现在回头看,这个需求本质上就是典型的备忘录模式应用场景。它的核心诉求很明确:在不破坏对象封装性的前提下,把对象在某个时刻的内部状态完整地保存下来,并且能在未来的某个时间点精确地恢复到那个状态。如果当时我先想清楚状态保存的边界、快照的存储方式、回退时对象的重建策略,后面那些坑至少能少踩一半。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 备忘录模式的本质:三个角色,各管一段
备忘录模式的定义很简洁:在不破坏封装的前提下,捕获并外部化一个对象的内部状态,以便之后可以将该对象恢复到该状态。它的结构也简单,就三个角色:发起人(Originator)、备忘录(Memento)和负责人(Caretaker)。
2.1 发起人:唯一有权存取状态的角色
发起人就是那个状态需要被保存的业务对象,比如我前面例子里的 Order。在备忘录模式里,发起人承担两个职责:生成备忘录,以及从备忘录恢复状态。
很多人第一次学这个模式的时候会有个疑问:生成备忘录和恢复状态,这不就是两个方法的事吗,为什么要单独抽象一个模式出来?关键在于"封装"这两个字。如果 Order 类把状态字段全部交给外部去存储,那外部就必须知道 Order 内部有哪些字段、哪些要存哪些不用存,一旦 Order 的结构变了,所有依赖它的存储代码都要跟着改。而备忘录模式把这两件事都收归 Order 自己处理,外部只负责保管备忘录对象,不需要关心里面的内容。
我整理了一张职责表,清晰区分这三个角色:
| 角色 | 职责 | 关键原则 |
|---|---|---|
| 发起人(Originator) | 创建备忘录、从备忘录恢复状态 | 只有自己能读写自己的内部状态 |
| 备忘录(Memento) | 保存发起人的状态快照 | 不暴露内部细节,外部拿不到修改入口 |
| 负责人(Caretaker) | 持有并管理备忘录对象的生命周期 | 只存不读,永远不知道备忘录里装了什么 |
2.2 备忘录:一份"只能读不能改"的存档
备忘录这个角色的核心设计点在于信息隐藏。它的理想状态是:发起人能往里写数据,也能从里面读数据,但负责人拿到的只是一份"存档文件",不能去修改或解析里面的内容。
在实际的编程语言里,这个理想状态有不同的落地方式。Java 里可以用嵌套类配合访问权限控制,让负责人无法访问备忘录内部字段;C++ 可以用 friend 声明;Python 这种没有强制访问控制的语言,就只能靠命名约定和程序员自觉了。我见过很多初学者写备忘录模式的时候,直接定义一个 Memento 类,里面一个字段对应发起人一个字段,然后 getter/setter 全部 public,负责人可以随意改动。这其实是把模式写歪了,备忘录的封装性一旦破了,状态被谁改的你都不知道,后面排查起来比 bug 本身还头疼。
2.3 负责人:只管保管,别多管闲事
负责人是备忘录的仓库管理员。它负责存放备忘录、维护历史记录链表、在需要的时候把对应版本的备忘录交给发起人。
