# 18 状态（行为型）

> 一个对象的行为随内部状态而变——同一个按钮在“播放中”和“已停止”时干的事完全不同。讲清状态模式怎么消灭满屏的 if(state==...)，以及它和策略模式那个“结构一样、灵魂不同”的经典辨析。

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

# 18 状态 · State（行为型）

上一篇迭代器管“遍历”。这一篇管“一个对象在不同状态下，行为完全不同”这件事——它和[策略](https://daiw.org/manual/design-patterns/strategy)是长得最像的一对，我们这次把它们彻底掰开。

## 一、这是什么：音乐播放器的那个按钮

音乐播放器上那个播放/暂停按钮，按下去会发生什么，**取决于它现在的状态**：

- 正在**播放**时按它 → 暂停。
- 已**暂停**时按它 → 继续播放。
- 已**停止**时按它 → 从头播放。

同一个操作(“按按钮”)，在不同状态下行为截然不同。而且状态之间还会**流转**：播放→暂停→播放→停止……红绿灯(红→绿→黄)、订单(待付款→已付款→已发货)、电梯，全是这种“行为随状态变、状态还会自动跳转”的东西。

状态模式就是：**把每一种状态封装成一个独立的对象，让对象把行为委托给“当前状态对象”。状态变了，行为自然跟着变；状态转换也由状态对象自己驱动。**

配套代码是音频播放器：`PlayingState`/`PausedState`/`StoppedState` 各自实现 `Play/Pause/Stop`,`AudioPlayer` 把操作全委托给“当前状态”。

## 二、解决什么痛：if(state==...) 地狱

不用状态模式，行为随状态变通常写成一堆条件判断，散落在每个方法里：

```go
// 痛点:每个方法都要把所有状态判断一遍,状态一多就是灾难
func (p *Player) Play() {
	if p.state == "playing" {
		fmt.Println("已经在播放了")
	} else if p.state == "paused" {
		p.state = "playing"; fmt.Println("恢复播放")
	} else if p.state == "stopped" {
		p.state = "playing"; fmt.Println("开始播放")
	}
}
func (p *Player) Pause() { /* 又把 3 种状态判断一遍 */ }
func (p *Player) Stop()  { /* 再把 3 种状态判断一遍 */ }
```

病灶清清楚楚：**同一套状态判断，在 `Play`/`Pause`/`Stop` 里各抄一遍**。加一个状态(比如“缓冲中”)，你得找出**每一个**方法，都加一个 `else if`——漏一个就是 bug。状态和行为的对应关系被打散成一地碎片。

<Callout type="info">
  **状态隔离的“会变的东西”，是“对象当前处于哪个状态，以及该状态下的行为”。** 它把“每个状态下该怎么响应各种操作”从散落的 `if-else` 里收拢，**一个状态一个类**，该状态的所有行为集中在一处。加状态 = 加一个类，而不是去每个方法里加分支。原本纵横交错的“状态×操作”矩阵，被整齐地切成了一列一列。
</Callout>

## 三、怎么实现：状态对象化，行为委托给当前状态

每个状态是一个实现了统一接口的对象，上下文持有“当前状态”，把操作转发给它；状态对象在响应时顺便把上下文切到下一个状态。

```go title="go/state/main.go（核心）"
type PlayerState interface {
	Play(player *AudioPlayer)
	Pause(player *AudioPlayer)
	Stop(player *AudioPlayer)
	Name() string
}

// 具体状态:播放中。它知道"在我这个状态下,各操作该怎么响应、该切到哪个状态"
type PlayingState struct{}
func (s *PlayingState) Play(p *AudioPlayer)  { fmt.Println("已经在播放了") }
func (s *PlayingState) Pause(p *AudioPlayer) {
	fmt.Println("暂停播放")
	p.SetState(&PausedState{})     // ← 状态自己驱动转换:播放中→已暂停
}

// 上下文:把操作全委托给"当前状态",自己不写任何 if
type AudioPlayer struct{ state PlayerState }
func (p *AudioPlayer) Play()  { p.state.Play(p) }
func (p *AudioPlayer) Pause() { p.state.Pause(p) }
```

`AudioPlayer` 的方法里再也没有状态判断了——它只是“把活派给当前状态”。加“缓冲中”状态？写个 `BufferingState` 实现接口即可，老状态一行不改。

## 四、有哪些坑

- **状态模式 vs 策略模式：全系列最经典的辨析，必须记牢。** 两者代码结构**一模一样**(上下文持有一个接口对象，把行为委托出去)。区别在灵魂：

  | | 策略 | 状态 |
  | --- | --- | --- |
  | **谁来切换** | **客户端**从外部主动选(“我要用信用卡”) | **状态对象**自己内部驱动(“暂停后自动进入已暂停”) |
  | **彼此知不知情** | 各策略**互不知道**对方 | 各状态**知道**下一个是谁(要负责转换) |
  | **意图** | 封装**可互换的算法** | 封装**随状态而变的行为 + 状态流转** |

  一句话：**策略是“你选一个算法用”，状态是“对象自己在几个状态间走来走去”。** 结构双胞胎，用途两码事。

- **状态转换逻辑放哪？** 上面是“状态对象自己决定下一个状态”(去中心化，加状态方便，但转换规则散在各状态里)。另一种是把“谁能转到谁”的规则集中放在上下文或一张转换表里(集中，规则清晰，但上下文变重)。状态多、转换复杂时，后者更好维护。
- **状态爆炸。** 状态特别多、转换关系特别密时，类的数量和它们之间的跳转会变得难以管理。这时该考虑正经的**状态机**建模(状态图 + 转换表)，甚至上状态机库/工作流引擎，而不是硬堆状态类。

## 五、开源里怎么用

- **TCP 连接状态机**：`LISTEN → SYN_SENT → ESTABLISHED → FIN_WAIT → CLOSED`……网络协议栈是状态模式(状态机)教科书级的应用，每个状态下对“收到某种包”的响应完全不同。
- **工作流 / 订单流转引擎**：订单、审批、工单的状态流转(待处理→处理中→已完成)，各类工作流引擎和 Go 的 FSM 库(如 `looplab/fsm`)就是把状态模式产品化。
- **游戏 AI**：敌人的“巡逻/追击/攻击/逃跑”状态机，每个状态下的行为和转换条件各异——游戏开发里状态机无处不在。
- **正则表达式引擎 / 词法分析器**：编译原理里的有限状态自动机(DFA/NFA)，本质就是状态模式的数学化身。

---

状态让对象“随状态改变行为”。下一篇[责任链](https://daiw.org/manual/design-patterns/chain-of-responsibility)：当一个请求可能由多个处理者之一负责、而你不想让发送者知道到底该谁处理时，把处理者串成一条链，让请求顺着链找到它的归宿——这正是所有 Web 中间件的骨架。
