你写的撤销功能,99% 是伪 Memento——Undo 不是存个备份那么简单 你写的撤销功能99% 是伪 Memento——Undo 不是存个备份那么简单几乎每个业务系统都有撤销需求但大多数实现跟 Memento 模式没什么关系。它们只是存个 JSON 快照到数据库然后在用户点撤销时把旧数据覆盖回去。这个做法能跑但它不是 Memento。Memento 的核心不是备份是封装状态的访问边界——让 Originator 自己管自己的状态外部Caretaker只负责存取一个黑盒不能偷看里面装了什么。这个区别在工程里会直接决定你的撤销系统能走多远。一个真实的踩坑场景三年前我接手一个电商后台的订单编辑系统。需求很简单运营改完订单信息后可以撤销回上一步。当时的实现是每次编辑前把整个 OrderDTO 序列化成 JSON 字符串塞进order_undo_log表。撤销时反序列化覆盖当前数据。看起来没毛病直到运营提了个需求撤销的时候能不能只恢复部分字段比如只改回收货地址但保留我刚改的价格。做不了。因为快照是整个 DTO 的粒度太粗。你想拆成字段级快照可以但 JSON 字符串里各个字段纠缠在一起外部系统Caretaker根本不知道哪个字段对应什么语义。更隐蔽的问题在后面OrderDTO 加了新字段旧快照反序列化回来新字段是 null。运营撤销后提交null 覆盖了别人刚填的数据。这就是 Caretaker 越界访问内部状态结构的代价——它根本不该知道 OrderDTO 长什么样。Memento 的真正结构Memento 模式三个角色Originator发起人拥有需要保存的状态负责创建和恢复 Memento。Memento备忘录封装状态对除 Originator 外的所有对象隐藏内部细节。Caretaker管理者负责保存 Memento不能操作或检查其内容。关键约束Caretaker 只能拿到一个 Opaque 的令牌不能拆包看内容。用 Java 写大概是这个结构java // Memento 是内部类或包私有外部只能拿到接口 public interface OrderMemento { // 空接口外部没有任何方法可调用 }public class OrderEditor { private String address; private BigDecimal price; private String remark;// 创建备忘录只有 Originator 知道自己怎么存 public OrderMemento save() { return new Snapshot(address, price, remark); } // 恢复只有 Originator 知道自己怎么恢复 public void restore(OrderMemento memento) { Snapshot s (Snapshot) memento; this.address s.address; this.price s.price; this.remark s.remark; } // Memento 实现是私有的外部完全不可见 private static class Snapshot implements OrderMemento { final String address; final BigDecimal price; final String remark; Snapshot(String a, BigDecimal p, String r) { this.address a; this.price p; this.remark r; } }}// Caretaker 只管存和取绝不拆开看 public class UndoManager { private final Deque stack new ArrayDeque();public void push(OrderMemento m) { stack.push(m); } public OrderMemento pop() { return stack.pop(); } public boolean isEmpty() { return stack.isEmpty(); }} 这个结构里UndoManager根本不知道OrderMemento里面有什么。它就是一个黑盒管理员。如果哪天OrderEditor内部状态重构了——比如把address拆成province/city/detail——UndoManager一行代码不用改。这就是封装的力量。JSON 快照方案牺牲了封装换来了简单但在长期演进中付出了十倍代价。跟数据库快照、Event Sourcing 的区别很多人把 Memento 和数据库快照混为一谈其实它们解决的是不同层面的问题。数据库快照是持久层的备份机制关注的是数据丢了怎么恢复。它的受众是 DBA 和运维粒度通常是整张表或整个库不涉及业务对象的封装边界。Memento是领域层的撤销机制关注的是用户在当前会话里的操作怎么回退。它的受众是业务对象本身粒度是单个对象的状态封装核心约束是 Caretaker 不能越界。Event Sourcing是另一种思路不存状态只存事件。撤销不是恢复旧状态而是追加一个逆向事件。这个方案更强大可以 replay、可以审计但复杂度也高一个数量级。Memento 是拍照片Event Sourcing 是记日记。选哪个取决于你的撤销需求有多复杂——如果只是简单的单步/多步撤销Memento 够用了如果需要完整的历史追溯和分支回放再考虑 Event Sourcing。三个工程化陷阱1. Memento 内存爆炸如果每次状态变更都存一个完整快照高频操作下内存很快撑爆。解决思路增量 Memento只存变更的字段不是整个对象。但这会打破黑盒原则——Caretaker 需要知道哪些字段变了。折中方案是 Originator 内部做增量计算对外仍然输出一个统一的黑盒。快照 操作日志每 N 步存一个完整快照中间用 Command 模式记录操作。撤销时先找最近快照再 replay 逆向操作。惰性复制利用不可变数据结构persistent data structure新旧状态共享未变更的部分物理上只复制变更的分支。2. 深拷贝 vs 引用泄露Memento 存的是对象引用还是深拷贝如果存引用Originator 后续修改会污染 Memento。如果存深拷贝大对象性能堪忧。没有银弹。我的习惯是值对象String、Integer、不可变 BigDecimal直接存引用集合和自定义对象必须深拷贝。Java 里可以用clone()、CopyOnWriteArrayList、或者 Jackson 序列化后再反序列化笨但稳。3. 多 Originator 的交叉恢复一个 Caretaker 管多个 Originator比如一个表单里有订单编辑器和客户编辑器撤销栈是统一的。用户点了撤销应该恢复哪个 Originator 的状态这种场景需要把 Command 模式拉进来每次用户操作包装成一个 CommandCommand 执行时各自创建 Memento。撤销栈里存的是 Command 对象pop 出来就知道该调用哪个 Originator 的 restore。java public interface EditCommand { void execute(); void undo(); }public class ChangePriceCommand implements EditCommand { private final OrderEditor editor; private OrderMemento backup; private final BigDecimal newPrice;public void execute() { backup editor.save(); // 执行前存快照 editor.setPrice(newPrice); } public void undo() { editor.restore(backup); }} Memento 管怎么存状态Command 管什么时候存、存谁的。两者配合才能搭一个工业级的撤销系统。什么时候该用 Memento不是有撤销需求就必须上 Memento。判断标准状态的内部结构可能变化 →用封装隔离变化Caretaker 不应知道状态细节安全/权限原因→用黑盒机制天然适合需要多级撤销且状态对象很大 →考虑增量 Memento 或 Command 组合只是简单的单字段编辑且状态结构极稳定 → 直接存旧值可能更轻量不必硬套模式设计模式不是炫技是在约束条件下做 trade-off。Memento 的约束是封装状态访问如果你的场景不需要这个约束强上模式反而是过度设计。我在做一个用卡皮巴拉讲设计模式的微信小程序「爪爪代码冒险记」23 个设计模式用漫画 答题的方式讲目前正在开发中。如果你觉得这类内容有意思搜一下「爪爪代码冒险记」或者等我后面的文章。

相关新闻

最新新闻

oh-my-zsh 终极指南:从安装到插件配置,打造高效命令行环境

oh-my-zsh 终极指南:从安装到插件配置,打造高效命令行环境

1. 项目概述:为什么你的终端需要一个“美化师”? 如果你每天都要和命令行终端打交道,无论是写代码、管理服务器还是处理数据,一个原始、朴素的终端界面可能很快就会让你感到乏味和低效。默认的终端提示符往往只显示当前路径&…

2026/8/17 5:20:49
美赛LaTeX模板深度解析:从核心构成到实战避坑指南

美赛LaTeX模板深度解析:从核心构成到实战避坑指南

1. 为什么美赛选手需要一份“趁手”的LaTeX模板?如果你正在准备参加美国大学生数学建模竞赛(MCM/ICM,俗称“美赛”),并且已经决定或者正在考虑使用LaTeX来撰写你的最终论文,那么你大概率已经听过“模板”这…

2026/8/17 5:20:49
SecureCRT日志时间戳配置:运维审计与故障排查的关键设置

SecureCRT日志时间戳配置:运维审计与故障排查的关键设置

1. 项目概述:为什么我们需要带时间的日志?做运维或者网络管理这行的朋友,对SecureCRT这款终端仿真软件肯定不陌生。它几乎是连接Linux服务器、网络设备(交换机、路由器)的标配工具。我们每天用它执行命令、查看输出、排…

2026/8/17 5:20:49
数学建模国赛培训:从讲座预告到实战转化的高效备赛指南

数学建模国赛培训:从讲座预告到实战转化的高效备赛指南

1. 从“预告”到“实战”:如何高效消化一场建模讲座看到“数学建模国赛线上培训讲座第三讲(预告)”这个标题,很多同学的第一反应可能是:哦,又有讲座了,到时候去听听。但作为一个带过好几届队伍、…

2026/8/17 5:20:49
Docker容器开机自启:从原理到生产环境配置与故障排查

Docker容器开机自启:从原理到生产环境配置与故障排查

1. 项目概述:为什么我们需要关心Docker开机自启?在服务器运维和日常开发中,我们部署在Docker容器里的应用,比如数据库、Web服务或者消息队列,最怕的就是服务器意外重启后,所有服务都需要手动一个个去拉起来…

2026/8/17 5:20:49
基于ReAct、RAG与Few-shot的意图识别系统架构与工程实践

基于ReAct、RAG与Few-shot的意图识别系统架构与工程实践

1. 项目概述:从“猜”到“懂”的意图识别进化做对话系统或者智能助手的朋友,对“意图识别”这四个字肯定又爱又恨。爱的是,它是整个交互的入口,决定了后续所有流程的走向;恨的是,它太容易出错了。用户说“帮…

2026/8/17 5:15:49