# 15 命令（行为型）

> 把“一个操作请求”本身打包成对象，于是它能被存储、排队、记录、撤销、重做。讲清命令模式怎么把“转瞬即逝的方法调用”变成“能攥在手里的东西”，以及 undo/redo、任务队列背后的它。

- 作者：David（道雾轩）
- 专栏：经典设计模式浅析（https://daiw.org/manual/design-patterns.md）
- 最后更新：2026-07-12
- 原文：https://daiw.org/manual/design-patterns/command
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 15 命令 · Command（行为型）

上一篇观察者把“事件”解耦。这一篇更进一步：**把“操作”本身变成一个可以攥在手里的对象。**

## 一、这是什么：餐厅的点菜单

你在餐厅点菜，服务员写下一张**点菜单**：桌号、菜品、数量。这张单子是个“实实在在的东西”——它可以被传到后厨、排进队列、几张单子一起处理、下错了能撤单。而“炒一盘宫保鸡丁”这个**动作**本身是抓不住的，一旦做了就过去了。点菜单的意义，就是把“要做什么”从“实际去做”里剥离成一个可保存、可传递的凭证。

命令模式就是这张点菜单：**把一个请求(要执行的操作 + 需要的参数)封装成一个对象。** 于是这个“操作”可以被存起来、排队、记日志、撤销、重做——这些都是“方法调用”这个转瞬即逝的动作做不到的。

配套代码是遥控器：每个按钮绑定一个命令对象(开灯、关灯)，按下就 `Execute()`，而且遥控器记着历史，能一路 `Undo()` 回去。

## 二、解决什么痛：方法调用“抓不住”

`light.On()` 是个动作，调完就没了。但现实里，你常常想对“操作”这件事做更多文章：

- **撤销/重做**：得记住“做过什么”，才能倒回去。
- **排队/延迟执行**：把操作攒起来，稍后或异步执行(任务队列)。
- **记录/重放**：把操作写进日志，崩溃后重放恢复(事务、WAL)。
- **参数化**：让“按钮”这种通用组件，能绑定任意操作，而不用为每个按钮写死逻辑。

这些都要求“操作”是个**能存、能传的对象**，而不是一次性的方法调用。命令模式就是把动作“对象化”。

<Callout type="info">
  **命令隔离的“会变的东西”，是“具体要执行什么操作”。** 它在“发起请求的人”(遥控器/按钮)和“真正干活的人”(灯)之间，插入一个“命令对象”。发起者只管 `Execute()`，完全不知道也不关心这个命令背后是开灯、关门还是发射火箭。于是同一个按钮能绑任意命令，操作还能被存起来秋后算账。
</Callout>

## 三、怎么实现：把操作 + 接收者打包

命令对象持有“接收者”(真正干活的对象)和“要调的操作”，实现统一的 `Execute()`/`Undo()`。调用者只面对命令接口。

```go title="go/command/main.go（核心）"
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` 时整体恢复——这就和[备忘录](https://daiw.org/manual/design-patterns/memento)模式搭上了(第 21 篇)。改一段文字很难“反操作”，只能存下改之前的样子。
- **命令多了，类会膨胀。** 每个操作一个命令类，操作一多，类爆炸。Go 里可以用**函数**(`type Command func()`)或闭包大幅简化，不必每个都建类型——和策略同款思路。
- **宏命令(组合命令)**：把多个命令打包成一个“一键执行全部”，本质是命令 + [组合模式](https://daiw.org/manual/design-patterns/composite)。“一键离家”(关灯+锁门+关空调)就是宏命令。
- **和策略分清：** 策略是“做**同一件事**的不同算法”(几种支付方式，可互换)；命令是“把**一个请求**打包成对象”(强调存储/撤销/排队)。策略关注“怎么做”，命令关注“把‘做什么’变成可管理的对象”。

## 五、开源里怎么用

- **编辑器/设计工具的 undo/redo**:VS Code、Photoshop、Figma 的历史记录，每一步操作都是一个命令对象压进撤销栈——命令模式最闪耀的舞台。
- **任务队列 / 异步作业**：把“要干的活”封成命令对象扔进队列，worker 取出来 `Execute`。Go 里往 channel 里发的 job、各类分布式任务框架(Celery、Sidekiq)的 task，都是命令。
- **事务与预写日志(WAL)**：数据库把每个操作记成日志条目(命令)，崩溃后按日志重放恢复，或按逆命令回滚——命令模式在存储引擎里的硬核应用。
- **Java `Runnable` / Go 的 `func()` 作任务**：把“一段要执行的逻辑”封成对象/闭包交给线程池或 goroutine——这本身就是命令模式最轻量的形态，你可能天天在用却没意识到。

---

命令把“操作”对象化。下一篇[模板方法](https://daiw.org/manual/design-patterns/template-method)：当一堆流程“骨架相同、只有个别步骤不同”时，怎么把骨架写一次、把变化的步骤留给各自填。
