18 状态(行为型)
一个对象的行为随内部状态而变——同一个按钮在“播放中”和“已停止”时干的事完全不同。讲清状态模式怎么消灭满屏的 if(state==...),以及它和策略模式那个“结构一样、灵魂不同”的经典辨析。
18 状态 · State(行为型)
上一篇迭代器管“遍历”。这一篇管“一个对象在不同状态下,行为完全不同”这件事——它和策略是长得最像的一对,我们这次把它们彻底掰开。
一、这是什么:音乐播放器的那个按钮
音乐播放器上那个播放/暂停按钮,按下去会发生什么,取决于它现在的状态:
- 正在播放时按它 → 暂停。
- 已暂停时按它 → 继续播放。
- 已停止时按它 → 从头播放。
同一个操作(“按按钮”),在不同状态下行为截然不同。而且状态之间还会流转:播放→暂停→播放→停止……红绿灯(红→绿→黄)、订单(待付款→已付款→已发货)、电梯,全是这种“行为随状态变、状态还会自动跳转”的东西。
状态模式就是:把每一种状态封装成一个独立的对象,让对象把行为委托给“当前状态对象”。状态变了,行为自然跟着变;状态转换也由状态对象自己驱动。
配套代码是音频播放器:PlayingState/PausedState/StoppedState 各自实现 Play/Pause/Stop,AudioPlayer 把操作全委托给“当前状态”。
二、解决什么痛:if(state==...) 地狱
不用状态模式,行为随状态变通常写成一堆条件判断,散落在每个方法里:
// 痛点:每个方法都要把所有状态判断一遍,状态一多就是灾难
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。状态和行为的对应关系被打散成一地碎片。
状态隔离的“会变的东西”,是“对象当前处于哪个状态,以及该状态下的行为”。 它把“每个状态下该怎么响应各种操作”从散落的 if-else 里收拢,一个状态一个类,该状态的所有行为集中在一处。加状态 = 加一个类,而不是去每个方法里加分支。原本纵横交错的“状态×操作”矩阵,被整齐地切成了一列一列。
三、怎么实现:状态对象化,行为委托给当前状态
每个状态是一个实现了统一接口的对象,上下文持有“当前状态”,把操作转发给它;状态对象在响应时顺便把上下文切到下一个状态。
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),本质就是状态模式的数学化身。
状态让对象“随状态改变行为”。下一篇责任链:当一个请求可能由多个处理者之一负责、而你不想让发送者知道到底该谁处理时,把处理者串成一条链,让请求顺着链找到它的归宿——这正是所有 Web 中间件的骨架。