4. 状态机实现模式:表驱动模式、状态模式、嵌套状态机、分层状态机
好,咱们进入正题。状态机的实现方式,说白了就是「怎么把状态转换的逻辑写进代码里」。我见过不少团队,一开始用最简单的 if-else 堆,堆到几百行之后彻底崩盘——改一个状态要翻遍整个文件,那叫一个酸爽。
今天聊的这四种模式,是我在实际项目中反复用过的。它们各有各的脾气,也各有各的适用场景。咱们一个一个来看。
4.1 表驱动模式:最直观,也最容易被低估
表驱动模式,核心思路就一句话:把状态转换规则塞进一张表里。代码里没有 if-else,只有查表。
我习惯用一个二维数组或者字典来定义这张表。行是当前状态,列是触发事件,交叉点就是下一个状态。
// 状态表定义
// 行:当前状态,列:事件
// 值:{ nextState, action }
const stateTable = {
'IDLE': {
'START': { nextState: 'RUNNING', action: 'onStart' },
'STOP': { nextState: 'IDLE', action: null }
},
'RUNNING': {
'PAUSE': { nextState: 'PAUSED', action: 'onPause' },
'STOP': { nextState: 'IDLE', action: 'onStop' },
'ERROR': { nextState: 'ERROR', action: 'onError' }
},
'PAUSED': {
'RESUME': { nextState: 'RUNNING', action: 'onResume' },
'STOP': { nextState: 'IDLE', action: 'onStop' }
},
'ERROR': {
'RESET': { nextState: 'IDLE', action: 'onReset' }
}
};
function transition(currentState, event) {
const row = stateTable[currentState];
if (!row) throw new Error(`未知状态: ${currentState}`);
const cell = row[event];
if (!cell) throw new Error(`状态 ${currentState} 不支持事件 ${event}`);
// 执行动作
if (cell.action) {
this[cell.action]();
}
return cell.nextState;
}
我在项目中遇到过一件事:一个支付系统的状态机,最初用 if-else 写了 300 多行。后来重构为表驱动,代码量直接砍到 80 行。而且产品经理改需求时,我只需要改表里的几个字段,不用动逻辑代码。
- 状态转换规则一目了然——你甚至可以让非技术人员看懂这张表
- 新增状态或事件,只需要加一行一列,不会破坏现有逻辑
- 容易做合法性校验——遍历表就能发现「死状态」和「不可达状态」
4.2 状态模式(GoF 设计模式):面向对象的优雅解法
状态模式,说白了就是把每个状态封装成一个类。每个类里只关心自己这个状态下的事件处理逻辑。
我记得第一次用状态模式是在一个网络连接管理模块里。连接有「已断开」「连接中」「已连接」「重连中」四个状态。如果用 if-else,每个方法里都要判断当前状态,代码会变得又臭又长。
// 状态接口
interface ConnectionState {
void connect(ConnectionContext ctx);
void disconnect(ConnectionContext ctx);
void send(ConnectionContext ctx, String data);
}
// 具体状态:已断开
class DisconnectedState implements ConnectionState {
void connect(ConnectionContext ctx) {
// 执行连接逻辑
ctx.setState(new ConnectingState());
}
void disconnect(ConnectionContext ctx) {
// 已经断开了,啥也不做
}
void send(ConnectionContext ctx, String data) {
throw new IllegalStateException("未连接,无法发送数据");
}
}
// 具体状态:已连接
class ConnectedState implements ConnectionState {
void connect(ConnectionContext ctx) {
// 已经连接了,忽略
}
void disconnect(ConnectionContext ctx) {
// 执行断开逻辑
ctx.setState(new DisconnectedState());
}
void send(ConnectionContext ctx, String data) {
// 正常发送数据
}
}
你想想看,每个状态类只负责自己的行为。新增一个状态,就新增一个类,不会动到其他状态的代码。这完全符合开闭原则。
不过状态模式也有坑。我曾经在一个项目中用了状态模式,结果状态类之间互相依赖,形成了循环引用。调试起来那叫一个头疼。所以我的建议是:状态类之间不要直接引用,所有状态转换都通过 Context 来协调。
4.3 嵌套状态机:状态里的状态
嵌套状态机,说白了就是一个状态内部还可以有一个完整的状态机。这玩意儿在 UI 交互和协议处理中特别常见。
举个例子:一个「编辑中」状态,内部可能还有「选中文本」「正在输入」「正在粘贴」等子状态。从外部看,整个系统处于「编辑中」状态。但从内部看,它自己有一套完整的状态转换逻辑。
// 嵌套状态机的结构示意
class EditorState {
String name = "EDITING";
// 内部状态机
StateMachine innerFSM;
EditorState() {
innerFSM = new StateMachine();
innerFSM.addState("TEXT_SELECTED");
innerFSM.addState("TYPING");
innerFSM.addState("PASTING");
innerFSM.setInitialState("TYPING");
}
void handleEvent(String event) {
// 先让内部状态机处理
boolean handled = innerFSM.handleEvent(event);
if (!handled) {
// 内部处理不了,再交给外部
// 比如收到 "SAVE" 事件,内部状态机不处理
// 由外部状态机处理
}
}
}
嵌套状态机的好处是:你可以把复杂的状态逻辑拆成多层。每一层只关心自己这一层的事情。我曾经在做一个聊天协议解析器时用了三层嵌套状态机,最外层是「连接状态」,中间层是「会话状态」,最内层是「消息解析状态」。每一层都清晰独立,调试起来非常方便。
4.4 分层状态机:把状态按层级组织
分层状态机和嵌套状态机有点像,但思路不同。分层状态机强调的是状态之间的继承关系。子状态可以继承父状态的行为,也可以覆盖它。
我举个例子你就明白了。一个机器人的状态机:
- 父状态:活跃状态——在这个状态下,机器人可以移动、可以说话
- 子状态:巡逻状态——继承活跃状态,额外增加了巡逻逻辑
- 子状态:战斗状态——继承活跃状态,额外增加了攻击逻辑
如果机器人处于「巡逻状态」,它既能巡逻,也能移动和说话(从父状态继承来的)。如果处于「战斗状态」,它既能攻击,也能移动和说话。
// 分层状态机示意
class HierarchicalState {
String name;
HierarchicalState parent;
void onEnter() { /* 进入状态时的逻辑 */ }
void onExit() { /* 退出状态时的逻辑 */ }
void handleEvent(String event) {
// 先看自己能不能处理
if (canHandle(event)) {
processEvent(event);
} else if (parent != null) {
// 自己处理不了,交给父状态
parent.handleEvent(event);
}
}
}
class PatrolState extends HierarchicalState {
PatrolState() {
this.name = "PATROL";
this.parent = new ActiveState(); // 继承自活跃状态
}
void handleEvent(String event) {
if ("ENEMY_SPOTTED".equals(event)) {
// 巡逻状态特有的事件处理
transitionTo(new CombatState());
} else {
// 其他事件交给父状态
super.handleEvent(event);
}
}
}
分层状态机的精髓在于「继承」和「委派」。子状态只关心自己特有的行为,通用的行为交给父状态。这样代码复用率极高。
- 状态之间有明显的「is-a」关系(巡逻状态是一种活跃状态)
- 多个状态共享大量相同的行为
- 你希望减少重复代码,但又不想用复杂的继承体系
我个人习惯在游戏 AI 和复杂业务流程中使用分层状态机。比如订单处理流程:所有订单状态都有「取消」和「查询」行为,那就把这些行为放到父状态里,子状态只关心自己特有的逻辑。
4.5 四种模式的对比与选择
好了,四种模式都聊完了。你可能会问:到底该用哪一种?
我的建议是:
| 模式 | 适用场景 | 状态数量 | 复杂度 |
|---|---|---|---|
| 表驱动模式 | 状态转换规则明确、变化频繁 | 少(<15) | 低 |
| 状态模式 | 每个状态行为差异大、需要扩展性 | 中(5-20) | 中 |
| 嵌套状态机 | 状态内部有独立的状态转换逻辑 | 不限 | 高 |
| 分层状态机 | 状态之间有继承关系、共享行为多 | 不限 | 中高 |
其实没有银弹。我在实际项目中经常混合使用:外层用表驱动做整体状态管理,内层用状态模式处理复杂状态的行为逻辑。这样既保证了整体结构的清晰,又保留了局部扩展的灵活性。
嗯,四种模式就聊到这儿。每种模式都有它的脾气,关键是你得知道什么时候该用哪一把刀。下一节咱们聊聊状态机的测试策略——这部分我踩过的坑可不少。
可可精品资料库