你写的撤销功能,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 个设计模式用漫画 答题的方式讲目前正在开发中。如果你觉得这类内容有意思搜一下「爪爪代码冒险记」或者等我后面的文章。

相关新闻

不可见只物:AI幻觉的拓扑学

不可见只物:AI幻觉的拓扑学

——为什么模型会在最自信的时候,编造它自己都信以为真的东西开篇宇宙中有一种定律。它没有被写入任何物理教科书,不作用于引力或电磁,不守恒任何量子数。它只是在那里——安静地、不可被完全解释地,存在于每一个足够复杂的系统里…

2026/10/9 11:42:23 阅读更多 →
从FNF定制框架到可迭代游戏项目:理解、跑通与工程化改造实战

从FNF定制框架到可迭代游戏项目:理解、跑通与工程化改造实战

上周在整理本地项目时,翻到一个名为“未命名-迷你世界fnf引擎”的文件夹。点开一看,里面是几个零散的脚本文件和资源包,注释里写着“引擎展示”。这让我想起一个挺有意思的现象:很多开发者,尤其是刚接触游戏制作的朋友…

2026/10/5 4:44:36 阅读更多 →
Wi-Fi MAC地址修改指南:绕过路由器黑名单与隐私保护实战

Wi-Fi MAC地址修改指南:绕过路由器黑名单与隐私保护实战

1. 从一次“断网”风波说起:为什么需要修改Wi-Fi MAC地址?事情是这样的,我家里有一台老路由器,管理后台的“设备拉黑”功能用得比较频繁,主要是为了限制家里小孩的某些设备在特定时段上网。有一次,我自己的…

2026/10/13 2:26:38 阅读更多 →

最新新闻

TMS WebCore v1.1.3.0源码包实战:Delphi XE10.3环境搭建与Web部署避坑指南

TMS WebCore v1.1.3.0源码包实战:Delphi XE10.3环境搭建与Web部署避坑指南

简介:TMS WebCore v1.1.3.0 完整源码包面向使用 Delphi XE10.3 的开发者,帮助其在不深入 HTML、CSS、JavaScript 的前提下,用熟悉的 VCL/FMX 技能构建现代响应式 Web 应用。包内共 1573 个文件,约 34.72MB,以 444 个 p…

2026/10/13 18:27:30 阅读更多 →
给水电站机电设备与自动化同学的 AI 论文工具帖:别让一个软件硬扛整套毕设 [特殊字符]

给水电站机电设备与自动化同学的 AI 论文工具帖:别让一个软件硬扛整套毕设 [特殊字符]

先把场景说具体。 水电站机电设备与自动化这个专业,平时学的东西很“跨界”:水轮机、发电机、辅机设备、PLC、传感器、监控系统、继电保护、故障诊断……哪一样都不是靠背概念就能完事的。到了毕业季,很多同学会遇到一类特别典型的任务&…

2026/10/13 18:27:30 阅读更多 →
第14届蓝桥杯Python省赛真题解析:从考点拆解到源码实战

第14届蓝桥杯Python省赛真题解析:从考点拆解到源码实战

简介:面向蓝桥杯Python省赛备战的完整真题与源码合集,内容围绕第14届省赛大学A、B、C三组展开,既适合初次接触比赛的选手快速熟悉命题风格和题型分布,也便于进阶选手在备赛中期按组别展开系统性刷题,逐题检验算法基础与…

2026/10/13 18:27:30 阅读更多 →
输电线路比载应力弧垂计算:MATLAB程序包从入门到工程验证

输电线路比载应力弧垂计算:MATLAB程序包从入门到工程验证

简介:面向输电线路设计与运行维护的MATLAB计算工具,可完成比载、应力和弧垂三类核心参数的联动求解。程序覆盖导线自重、风荷载、冰荷载等工况下的比载计算,并根据张力与应力状态,结合热弹性理论与结构力学方法求解不同温度下的弧…

2026/10/13 18:27:30 阅读更多 →
固定资产管理系统Java源码部署指南:从环境配置到二次开发

固定资产管理系统Java源码部署指南:从环境配置到二次开发

简介:一套面向Java Web开发者与在校学生的固定资产管理系统完整项目源码,聚焦企业固定资产全生命周期管理,覆盖资产增删改查、采购入库、折旧盘点等常见业务场景,可直接运行或二次开发。项目基于Java实现,默认支持SQL …

2026/10/13 18:27:30 阅读更多 →
Hadoop DataNode排除机制深度解析:3 running, 3 excluded真相

Hadoop DataNode排除机制深度解析:3 running, 3 excluded真相

1. 这条报错不是“节点挂了”,而是Hadoop在认真执行你的排除策略刚看到这条日志——“There are 3 datanode(s) running and 3 node(s) are excluded in this operation”——我第一反应是:完了,集群全跪了?赶紧去jps看进程、查hd…

2026/10/13 18:26:28 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →