大家好,我是十三!欢迎来到十三Tech。
前阵子做一个富文本编辑器——用户能输入文字、删除、粘贴、改样式,每步操作都要支持撤销。最初的代码是直接调 editor.AddText() / editor.DeleteText(),撤销时傻眼了——"删除"怎么撤销?得知道删了什么内容;"改样式"怎么撤销?得知道改之前的样式是什么。
后来把每个操作封装成一个 Command 对象——AddTextCmd 知道加了什么文本,DeleteTextCmd 在删除时记录了被删的内容。撤销时调 cmd.Undo() 反向操作。一个 History 栈管理所有 Command,撤销就是出栈反向执行。
这就是命令模式——把请求封装成对象,让请求获得排队、撤销、日志等能力。但很多人觉得"封装请求"很抽象——封装了能干啥?这一篇就聊聊命令的标志特征是"支持撤销",以及它和策略的真实差别。
一、命令在解决什么——不是"封装方法调用"
教科书定义:将请求封装成对象,从而让你用不同的请求对客户端参数化。这话抽象——封装请求有什么用?真正的命令有一个标志特征——支持撤销/排队/日志。
如果一个对象只是把方法调用包装一层(type DoSomethingCmd struct{}; func (c *DoSomethingCmd) Execute() { foo() }),这是过度设计——直接调 foo() 不就完了。命令的价值在于"请求成为一等公民"——可以被压栈、可以延迟执行、可以记录、可以传输、可以撤销。
判断口诀:需要撤销/排队/日志/传输 → 命令;只是封装方法调用 → 直接调函数。
命令和策略最容易混——都把行为封装成对象。但意图不同:策略关心"算法可替换",命令关心"操作的历史"。策略换了就换了,旧策略丢掉;命令要保留历史(撤销栈),每次 Execute 都进栈。策略是当下的选择,命令是历史的记录。
二、命令模式怎么写——Execute + Undo + History
命令模式的核心是 Command 接口有 Execute() 和 Undo() 两个方法,Invoker 用栈管理 Command 历史。
type Command interface {
Execute()
Undo()
}
type AddTextCmd struct {
editor *Editor
text string
}
func (c *AddTextCmd) Execute() { c.editor.AddText(c.text) }
func (c *AddTextCmd) Undo() { c.editor.DeleteText(len(c.text)) }
type History struct {
done []Command
undone []Command
}
func (h *History) Execute(c Command) {
c.Execute()
h.done = append(h.done, c)
h.undone = nil
}
看 History.Execute——执行 Command、压栈、清空 redo 栈(执行新操作后无法再 redo 旧分支)。Undo 是出栈反向调用。Command 对象自己保存"撤销需要的信息"——DeleteTextCmd 在 Execute 时把被删内容存到 c.deleted 字段,Undo 时把这段内容 AddText 回来。
写命令模式有个工程坑:Undo 必须严格反向。如果 Execute 做了 A、B、C 三件事,Undo 必须按 C'、B'、A' 顺序反向执行。漏一步或多一步都会导致状态不一致。
三、什么时候别用命令模式
命令模式不是所有"操作"场景都适用,至少有三个反模式。
反模式一:操作简单且无撤销需求。如果操作就是 user.Save(),没有撤销、没有排队、没有日志——直接调函数。包一层 Command 徒增样板代码。
反模式二:Command 不保存撤销信息。如果 Command 的 Undo 实现是 // TODO 或者只能"标记无效"不能真正反向——这不是命令模式,这是事件日志。命令模式要求 Undo 能真正恢复到 Execute 之前的状态。
反模式三:撤销栈无限制增长。每次 Execute 都进栈,栈永远不清理——内存吃光。常见做法是限制栈深度(保留最近 50 步),超过就丢最早的。
Go 标准库里命令思想最典型的地方是 database/sql.Tx——tx.Begin() 创建事务(构造 Command 对象),tx.Commit() 执行(持久化所有操作),tx.Rollback() 撤销(回滚所有操作)。事务就是一组命令的原子化封装——要么全部 Execute 成功,要么全部 Undo 回滚。看 database/sql 的 Tx 实现源码,命令模式思想清晰可见。
回头看那个富文本编辑器——值得用命令模式吗?值得,每步操作都要可撤销、操作类型有限(增删改样式)、撤销栈需要管理,正是命令模式的甜区。但如果只是简单的"保存"操作没有撤销,直接调函数更直接。
命令模式真正教会我的,不是"封装请求",而是让操作拥有历史——直接调函数是"当下执行就完了",命令模式让每次执行都成为可追溯、可回滚的事件。这个"历史化"思维比模式本身重要得多。
下一篇讲迭代器模式,它把"遍历"从容器中剥离,让容器无需关心遍历细节。
关于十三Tech 资深服务端研发,AI 实践者,专注分享真实可落地的技术经验。 相信 AI 是程序员的最佳搭档。
联系方式:569893882@qq.com GitHub:@TriTechAI
