18 状态(行为型)

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

作者 David更新于

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 里收拢,一个状态一个类,该状态的所有行为集中在一处。加状态 = 加一个类,而不是去每个方法里加分支。原本纵横交错的“状态×操作”矩阵,被整齐地切成了一列一列。

三、怎么实现:状态对象化,行为委托给当前状态

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

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),本质就是状态模式的数学化身。

状态让对象“随状态改变行为”。下一篇责任链:当一个请求可能由多个处理者之一负责、而你不想让发送者知道到底该谁处理时,把处理者串成一条链,让请求顺着链找到它的归宿——这正是所有 Web 中间件的骨架。

本页目录