状态模式:让对象随着状态起舞
状态模式,说白了就是让一个对象在内部状态变化时,自动切换自己的行为。你想想看,就像一个人——开心的时候会笑,难过的时候会哭,生气的时候会吼。同一个人,状态不同,行为完全不同。
我记得刚工作那会儿,接手过一个订单系统。订单有各种状态:待支付、已支付、已发货、已完成、已取消。每个状态下,用户能做的操作都不一样。比如待支付可以取消,已支付就不能取消了。当时代码写得那叫一个痛苦——满屏的 if-else,改一个状态就要动好几个地方。
后来我学了状态模式,才明白:把每个状态封装成独立的类,让状态自己决定行为。这才是正道。
核心思想
状态模式的核心就一句话:将状态相关的行为封装到独立的状态类中。上下文对象不再自己判断状态,而是把行为委托给当前的状态对象。
这样做的好处很明显:
- 消除条件分支——不用再写大段的 if-else 或 switch-case
- 状态转换清晰——每个状态类只关心自己的转换逻辑
- 易于扩展——加一个新状态,加一个新类就行,不用改现有代码
关键点:状态模式让对象看起来像是改变了它的类。因为行为完全变了,外部调用者感觉就像换了一个对象。
结构图
先看一张图,理解状态模式的整体结构:
代码示例
拿一个简单的播放器来说。播放器有三种状态:播放中、暂停中、停止中。每种状态下,点击播放按钮的行为都不一样。
// 状态接口
interface PlayerState {
play(player: MediaPlayer): void;
pause(player: MediaPlayer): void;
stop(player: MediaPlayer): void;
}
// 播放中状态
class PlayingState implements PlayerState {
play(player: MediaPlayer): void {
console.log("已经在播放了,别点了");
}
pause(player: MediaPlayer): void {
console.log("暂停播放");
player.setState(new PausedState());
}
stop(player: MediaPlayer): void {
console.log("停止播放");
player.setState(new StoppedState());
}
}
// 暂停中状态
class PausedState implements PlayerState {
play(player: MediaPlayer): void {
console.log("继续播放");
player.setState(new PlayingState());
}
pause(player: MediaPlayer): void {
console.log("已经是暂停状态了");
}
stop(player: MediaPlayer): void {
console.log("停止播放");
player.setState(new StoppedState());
}
}
// 停止中状态
class StoppedState implements PlayerState {
play(player: MediaPlayer): void {
console.log("从头开始播放");
player.setState(new PlayingState());
}
pause(player: MediaPlayer): void {
console.log("没在播放,不能暂停");
}
stop(player: MediaPlayer): void {
console.log("已经是停止状态了");
}
}
// 上下文类
class MediaPlayer {
private state: PlayerState;
constructor() {
this.state = new StoppedState();
}
setState(state: PlayerState): void {
this.state = state;
}
play(): void {
this.state.play(this);
}
pause(): void {
this.state.pause(this);
}
stop(): void {
this.state.stop(this);
}
}
// 使用示例
const player = new MediaPlayer();
player.play(); // 输出:从头开始播放
player.pause(); // 输出:暂停播放
player.play(); // 输出:继续播放
player.stop(); // 输出:停止播放
player.pause(); // 输出:没在播放,不能暂停
我的习惯:写状态模式时,我会先画一张状态转换图。把每个状态能做什么、能转到什么状态都列清楚。代码写起来就顺了。
状态转换表
为了方便理解,我把播放器的状态转换整理成了一张表:
| 当前状态 | 操作 | 结果行为 | 下一个状态 |
|---|---|---|---|
| 停止中 | 播放 | 从头开始播放 | 播放中 |
| 停止中 | 暂停 | 没在播放,不能暂停 | 停止中 |
| 停止中 | 停止 | 已经是停止状态 | 停止中 |
| 播放中 | 播放 | 已经在播放了 | 播放中 |
| 播放中 | 暂停 | 暂停播放 | 暂停中 |
| 播放中 | 停止 | 停止播放 | 停止中 |
| 暂停中 | 播放 | 继续播放 | 播放中 |
| 暂停中 | 暂停 | 已经是暂停状态 | 暂停中 |
| 暂停中 | 停止 | 停止播放 | 停止中 |
适用场景
状态模式不是万能的,它适合以下情况:
- 对象的行为依赖于它的状态,并且状态会变化
- 状态数量不多,但每个状态下的行为逻辑比较复杂
- 状态转换规则明确,可以画成状态图
- 你想避免大量的条件分支,让代码更干净
我曾经踩过的坑:有一次我把状态模式用在了状态数量超过20个的场景上。结果状态类爆炸,维护起来比 if-else 还痛苦。后来我改用状态表+策略模式才搞定。记住:状态模式适合状态数量可控的场景,一般不超过10个。
与策略模式的区别
很多人会把状态模式和策略模式搞混。我简单说一下:
- 策略模式:客户端主动选择策略,策略之间是平级的,互不依赖
- 状态模式:状态自动转换,状态之间有关联,一个状态知道下一个状态是什么
说白了,策略模式是「我想用哪个用哪个」,状态模式是「到了什么状态就做什么事」。
实际项目中的应用
我在项目中用过状态模式的地方:
- 订单状态机:待支付→已支付→已发货→已完成,每个状态下的操作不同
- 工作流引擎:审批流程中的待审批→审批中→已通过/已驳回
- 游戏角色:角色的站立、行走、奔跑、跳跃状态,每个状态下的动画和物理行为不同
- 网络连接:已连接、连接中、已断开、重连中
嗯,这里要注意一点:状态模式虽然好,但不要滥用。如果你的状态只有两三个,而且每个状态下的行为很简单,那用 if-else 反而更直接。设计模式是为了解决问题,不是为了炫技。
总结一下:状态模式把状态相关的行为封装到独立的状态类中,让对象在内部状态改变时自动切换行为。它消除了条件分支,让代码更清晰、更易扩展。但要注意控制状态数量,别让状态类泛滥成灾。
我个人习惯在项目初期就设计好状态转换图。哪怕后面需求变了,改状态图比改满屏的 if-else 要轻松得多。你想想看,是不是这个道理?