命令模式核心是将操作封装为可存、可调、可倒带的对象,必须实现对称的execute()和undo(),且undo所需状态须在构造或execute前捕获并持有,不可现场读取;通过双栈管理历史,栈中存弱引用或纯数据命令,限制大小并统一调度;异步与不可逆操作需预存快照或设计补偿逻辑。

核心是把“做什么”变成一个可存、可调、可倒带的对象,而不是直接写方法调用。关键不在封装动作本身,而在于命令对象能否独立持有执行前状态,并提供对称的 Execute() 和 Undo() 行为。
命令接口必须包含两个方法且自持上下文
定义接口时不能只写 Execute(),必须显式加上 Undo()。更重要的是,命令对象在构造或执行前就要捕获必要快照——比如当前数值、文件内容、UI 元素原始属性值,而不是在 Undo() 里临时去读取当前状态。
- 错误做法:Execute() 中调用 receiver.SetName("new"),Undo() 再调用 receiver.SetName("old"),但 "old" 是从控件里现场取的,控件重建后就失效
- 正确做法:构造命令时传入原始值,或在 Execute() 开头主动保存一份副本(如 string oldName = receiver.Name)并存为字段
- 接收者(Receiver)只暴露原子操作,不参与撤销逻辑;所有状态管理由命令自己完成
用双栈管理历史,注意语义与内存安全
撤销重做不是单个列表来回跳,而是两个独立栈:undoStack 存已执行的命令,redoStack 存刚被 Undo() 弹出的命令。
- 每次新执行命令后,必须清空 redoStack——用户做了新操作,之前的“重做”路径就断了
- 栈中存命令对象引用,若命令强持有窗体、大图、文档全文等,会导致内存泄漏;应改用 WeakReference 包装 UI 引用,或彻底剥离 UI,只操作数据模型
- 限制栈大小(如最多保留 100 条),超出时移除最早命令,避免无节制增长
调用者需统一调度,不依赖具体 UI 框架机制
WPF 的 Button.Command 属性虽支持 ICommand,但默认不集成 Undo 逻辑;WinForms 更无原生支持。不能指望绑定自动触发撤销,得手动控制流程。
- 点击按钮时,创建对应命令实例(如 new InsertTextCommand(model, cursor, "hello")),调用其 Execute(),再 push 到 undoStack
- “撤销”按钮点击时,pop undoStack 调用 Undo(),再将该命令 push 到 redoStack
- “重做”按钮点击时,pop redoStack 调用 Execute(),再 push 回 undoStack
- 所有命令通过同一套 Invoker 或 Manager 统一调度,确保行为一致
异步与不可逆操作要提前设计补偿逻辑
不是所有操作天然可逆。文件保存、网络请求、数据库提交等,无法靠“反向调用”还原,必须预存原始数据或引入补偿动作。
- 保存文件前,先读取原文本并缓存进命令对象;Undo() 时不是删文件,而是把原文本写回去
- 发送 HTTP 请求的命令,Undo() 不是发 DELETE,而是记录响应结果+原始参数,用于界面回滚显示,或触发业务层补偿接口
- 对明确不可逆的操作(如删除云存储对象),应在 UI 层禁用 Undo,或弹确认框提示“此操作无法撤销”










