如何通过闭包机制实现撤销重做功能的命令模式?
- 内容介绍
- 相关推荐
本文共计750个文字,预计阅读时间需要3分钟。
封装是实现轻量级命令模式最自然的方式——它将执行动作和回滚逻辑都封装为函数,同时自然地捕捉执行时的状态快照,避免后期读取已变更的变量。
用两个闭包字段定义命令对象
不定义接口或抽象类,直接用结构体持有一对函数:
- Do:执行操作的闭包,内部已捕获所需状态(如原始值、ID、坐标等)
- Undo:撤销操作的闭包,同样在创建时就绑定反向所需数据,不是运行时再去查
- 例如移动小球:Do 里记录目标位置并触发动画;Undo 里直接用创建时捕获的
oldPos回退,而不是现场读 DOM
用 slice 管理线性历史栈
撤销/重做本质是时间序列,必须用可索引、可弹出的顺序容器:
- 维护
history []Command:所有已执行且未被撤销的命令 - 维护
redoStack []Command:从 history 弹出后暂存的命令,等待重做 - 每次新命令执行前,清空
redoStack——用户做了新操作,旧的重做路径就失效了 - Undo:从
history末尾 pop,执行其Undo(),再 push 到redoStack - Redo:从
redoStack末尾 pop,执行其Do(),再 push 到history
闭包必须捕获执行时刻的完整快照
这是撤销准确性的核心,也是最容易出错的地方:
- 不要在闭包里引用外部指针(如
*User)或全局变量(如db),否则 Undo 时操作的是当前最新状态 - 应在创建命令时显式拷贝关键字段:原值、ID、坐标、文本内容、枚举状态等
- 比如风扇调速命令,要立刻保存
preSpeed = fan.GetSpeed(),而不是等到 Undo 时再读 - 对大对象(如图像、文档),可存哈希或版本号,配合外部存储做按需还原
按需扩展:支持参数化或异步 Undo
简单场景 func() 足够,复杂逻辑可升级类型:
- 若 Undo 需访问上下文,改用
type UndoFunc func(context.Context),但初始化时仍要传入所需数据 - 异步操作(如保存文件)不能靠“倒放”,而应预存原始字节或快照路径,Undo 时替换回磁盘内容
- 不可逆操作(如发 HTTP 请求)可标记为
CanUndo: false,或设计伪可逆逻辑(如发撤回请求)
本文共计750个文字,预计阅读时间需要3分钟。
封装是实现轻量级命令模式最自然的方式——它将执行动作和回滚逻辑都封装为函数,同时自然地捕捉执行时的状态快照,避免后期读取已变更的变量。
用两个闭包字段定义命令对象
不定义接口或抽象类,直接用结构体持有一对函数:
- Do:执行操作的闭包,内部已捕获所需状态(如原始值、ID、坐标等)
- Undo:撤销操作的闭包,同样在创建时就绑定反向所需数据,不是运行时再去查
- 例如移动小球:Do 里记录目标位置并触发动画;Undo 里直接用创建时捕获的
oldPos回退,而不是现场读 DOM
用 slice 管理线性历史栈
撤销/重做本质是时间序列,必须用可索引、可弹出的顺序容器:
- 维护
history []Command:所有已执行且未被撤销的命令 - 维护
redoStack []Command:从 history 弹出后暂存的命令,等待重做 - 每次新命令执行前,清空
redoStack——用户做了新操作,旧的重做路径就失效了 - Undo:从
history末尾 pop,执行其Undo(),再 push 到redoStack - Redo:从
redoStack末尾 pop,执行其Do(),再 push 到history
闭包必须捕获执行时刻的完整快照
这是撤销准确性的核心,也是最容易出错的地方:
- 不要在闭包里引用外部指针(如
*User)或全局变量(如db),否则 Undo 时操作的是当前最新状态 - 应在创建命令时显式拷贝关键字段:原值、ID、坐标、文本内容、枚举状态等
- 比如风扇调速命令,要立刻保存
preSpeed = fan.GetSpeed(),而不是等到 Undo 时再读 - 对大对象(如图像、文档),可存哈希或版本号,配合外部存储做按需还原
按需扩展:支持参数化或异步 Undo
简单场景 func() 足够,复杂逻辑可升级类型:
- 若 Undo 需访问上下文,改用
type UndoFunc func(context.Context),但初始化时仍要传入所需数据 - 异步操作(如保存文件)不能靠“倒放”,而应预存原始字节或快照路径,Undo 时替换回磁盘内容
- 不可逆操作(如发 HTTP 请求)可标记为
CanUndo: false,或设计伪可逆逻辑(如发撤回请求)

