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 行。而且产品经理改需求时,我只需要改表里的几个字段,不用动逻辑代码。

核心优势:
  • 状态转换规则一目了然——你甚至可以让非技术人员看懂这张表
  • 新增状态或事件,只需要加一行一列,不会破坏现有逻辑
  • 容易做合法性校验——遍历表就能发现「死状态」和「不可达状态」
注意:表驱动模式不适合状态数量特别大的场景。比如状态有 50 个,事件有 30 个,那张表就是 1500 个格子,维护起来反而痛苦。我一般建议状态数不超过 15 个时用表驱动。

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) {
    // 正常发送数据
  }
}

你想想看,每个状态类只负责自己的行为。新增一个状态,就新增一个类,不会动到其他状态的代码。这完全符合开闭原则。

我的经验:状态模式适合状态数量中等(5-20个)、每个状态的行为差异较大的场景。如果每个状态的行为都很相似,用状态模式反而显得过度设计。

不过状态模式也有坑。我曾经在一个项目中用了状态模式,结果状态类之间互相依赖,形成了循环引用。调试起来那叫一个头疼。所以我的建议是:状态类之间不要直接引用,所有状态转换都通过 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) 中
嵌套状态机 状态内部有独立的状态转换逻辑 不限 高
分层状态机 状态之间有继承关系、共享行为多 不限 中高

其实没有银弹。我在实际项目中经常混合使用:外层用表驱动做整体状态管理,内层用状态模式处理复杂状态的行为逻辑。这样既保证了整体结构的清晰,又保留了局部扩展的灵活性。

一个小技巧:不管用哪种模式,先把状态转换图画出来。画图的过程就是梳理逻辑的过程。图都画不清楚,代码写出来肯定是一团乱麻。

嗯,四种模式就聊到这儿。每种模式都有它的脾气,关键是你得知道什么时候该用哪一把刀。下一节咱们聊聊状态机的测试策略——这部分我踩过的坑可不少。

状态机实现模式对比 表驱动模式 状态模式 嵌套状态机 分层状态机 二维表/字典 状态数 < 15 规则一目了然 易维护易校验 适合:规则明确 每个状态一个类 状态数 5-20 行为差异大 开闭原则 适合:扩展性强 状态内有状态机 状态数不限 多层嵌套 事件穿透 适合:协议解析 状态继承关系 状态数不限 行为继承/覆盖 代码复用高 适合:游戏AI 选择建议:小规模用表驱动,中规模用状态模式,复杂场景嵌套+分层 实际项目中的混合使用 外层:表驱动模式 → 整体状态管理 内层:状态模式 → 复杂状态行为 局部:嵌套/分层 → 处理特殊逻辑

可可精品资料库