15 命令(行为型)
把“一个操作请求”本身打包成对象,于是它能被存储、排队、记录、撤销、重做。讲清命令模式怎么把“转瞬即逝的方法调用”变成“能攥在手里的东西”,以及 undo/redo、任务队列背后的它。
15 命令 · Command(行为型)
上一篇观察者把“事件”解耦。这一篇更进一步:把“操作”本身变成一个可以攥在手里的对象。
一、这是什么:餐厅的点菜单
你在餐厅点菜,服务员写下一张点菜单:桌号、菜品、数量。这张单子是个“实实在在的东西”——它可以被传到后厨、排进队列、几张单子一起处理、下错了能撤单。而“炒一盘宫保鸡丁”这个动作本身是抓不住的,一旦做了就过去了。点菜单的意义,就是把“要做什么”从“实际去做”里剥离成一个可保存、可传递的凭证。
命令模式就是这张点菜单:把一个请求(要执行的操作 + 需要的参数)封装成一个对象。 于是这个“操作”可以被存起来、排队、记日志、撤销、重做——这些都是“方法调用”这个转瞬即逝的动作做不到的。
配套代码是遥控器:每个按钮绑定一个命令对象(开灯、关灯),按下就 Execute(),而且遥控器记着历史,能一路 Undo() 回去。
二、解决什么痛:方法调用“抓不住”
light.On() 是个动作,调完就没了。但现实里,你常常想对“操作”这件事做更多文章:
- 撤销/重做:得记住“做过什么”,才能倒回去。
- 排队/延迟执行:把操作攒起来,稍后或异步执行(任务队列)。
- 记录/重放:把操作写进日志,崩溃后重放恢复(事务、WAL)。
- 参数化:让“按钮”这种通用组件,能绑定任意操作,而不用为每个按钮写死逻辑。
这些都要求“操作”是个能存、能传的对象,而不是一次性的方法调用。命令模式就是把动作“对象化”。
命令隔离的“会变的东西”,是“具体要执行什么操作”。 它在“发起请求的人”(遥控器/按钮)和“真正干活的人”(灯)之间,插入一个“命令对象”。发起者只管 Execute(),完全不知道也不关心这个命令背后是开灯、关门还是发射火箭。于是同一个按钮能绑任意命令,操作还能被存起来秋后算账。
三、怎么实现:把操作 + 接收者打包
命令对象持有“接收者”(真正干活的对象)和“要调的操作”,实现统一的 Execute()/Undo()。调用者只面对命令接口。
type Command interface {
Execute() string
Undo() string
}
// 具体命令:持有接收者(light),把"开灯"打包;Undo 就是反操作"关灯"
type LightOnCommand struct{ light *Light }
func (c *LightOnCommand) Execute() string { return c.light.On() }
func (c *LightOnCommand) Undo() string { return c.light.Off() }
// 调用者:只认 Command 接口,并记录历史以支持撤销
type RemoteControl struct{ history []Command }
func (r *RemoteControl) PressButton(cmd Command) {
fmt.Println(cmd.Execute())
r.history = append(r.history, cmd) // ← 存起来,为了能撤销
}
func (r *RemoteControl) PressUndo() {
last := r.history[len(r.history)-1]
r.history = r.history[:len(r.history)-1]
fmt.Println("撤销:", last.Undo()) // ← 倒着回去
}因为操作被存进了 history,撤销就是把最近的命令挨个 Undo()——这正是所有编辑器 Ctrl+Z 的原理骨架。
四、有哪些坑
- 撤销要存够“回退所需的状态”。
Undo有两种实现:一是反操作(开灯↔关灯,像配套代码这样,简单);二是操作不可逆时,得在Execute前存快照,Undo时整体恢复——这就和备忘录模式搭上了(第 21 篇)。改一段文字很难“反操作”,只能存下改之前的样子。 - 命令多了,类会膨胀。 每个操作一个命令类,操作一多,类爆炸。Go 里可以用函数(
type Command func())或闭包大幅简化,不必每个都建类型——和策略同款思路。 - 宏命令(组合命令):把多个命令打包成一个“一键执行全部”,本质是命令 + 组合模式。“一键离家”(关灯+锁门+关空调)就是宏命令。
- 和策略分清: 策略是“做同一件事的不同算法”(几种支付方式,可互换);命令是“把一个请求打包成对象”(强调存储/撤销/排队)。策略关注“怎么做”,命令关注“把‘做什么’变成可管理的对象”。
五、开源里怎么用
- 编辑器/设计工具的 undo/redo:VS Code、Photoshop、Figma 的历史记录,每一步操作都是一个命令对象压进撤销栈——命令模式最闪耀的舞台。
- 任务队列 / 异步作业:把“要干的活”封成命令对象扔进队列,worker 取出来
Execute。Go 里往 channel 里发的 job、各类分布式任务框架(Celery、Sidekiq)的 task,都是命令。 - 事务与预写日志(WAL):数据库把每个操作记成日志条目(命令),崩溃后按日志重放恢复,或按逆命令回滚——命令模式在存储引擎里的硬核应用。
- Java
Runnable/ Go 的func()作任务:把“一段要执行的逻辑”封成对象/闭包交给线程池或 goroutine——这本身就是命令模式最轻量的形态,你可能天天在用却没意识到。
命令把“操作”对象化。下一篇模板方法:当一堆流程“骨架相同、只有个别步骤不同”时,怎么把骨架写一次、把变化的步骤留给各自填。