状态机设计理念与实现

状态机设计理念与实现

状态机的核心不是简单地修改一个状态字段,而是先定义状态之间的合法关系,再让所有状态变更都经过这套关系校验。它解决两个问题:

  1. 当前状态能否转换成目标状态。
  2. 状态变更时应该如何统一校验、记录和执行后续逻辑。

可以把状态机理解为一张有向图:每个状态是图里的节点,状态之间允许发生的转换是节点之间的边。只要某条边存在,就说明这个转换是合法的;如果边不存在,就说明不能从当前状态直接变更到目标状态。

第一步:设计状态机

设计状态机时,先不要急着写 if/else。应该先回答三个问题:

  1. 系统里有哪些明确的状态。
  2. 每个状态可以流转到哪些状态。
  3. 发生状态变更时,是否需要校验条件、执行动作或记录日志。

例如存在四个状态:

text 复制代码
A, B, C, D

如果它们之间可以互相转换,但不能转换到自身,那么状态关系可以表示为:

text 复制代码
A -> B
A -> C
A -> D
B -> A
B -> C
B -> D
C -> A
C -> B
C -> D
D -> A
D -> B
D -> C

这里的设计重点是:状态变更必须被规则驱动。业务代码不应该到处直接写 status = nextStatus,而是应该调用统一的状态机入口,让状态机判断这次变更是否合法。

状态机的职责可以拆成三层:

  1. 状态定义:列出所有可能出现的状态。
  2. 流转规则:描述哪些状态之间可以转换。
  3. 流转执行:在合法的前提下完成状态变更,并处理校验、日志、事件等副作用。

这样做的好处是状态关系集中、可读、可测试,后续新增状态或调整规则时,不需要在业务代码里到处寻找分散的判断逻辑。

第二步:设计数据结构

状态机可以使用图的数据结构进行描述。最直接的方式是使用邻接矩阵。

假设横向表示目标状态,纵向表示当前状态,那么矩阵中第 A 行第 B 列的值代表:

text 复制代码
当前状态 A 是否允许转换成目标状态 B

示例矩阵如下:

当前状态 \ 目标状态 A B C D
A 0 1 1 1
B 1 0 1 1
C 1 1 0 1
D 1 1 1 0

其中:

  • 1 表示允许转换。
  • 0 表示不允许转换。
  • 横看代表状态的出度。
  • 纵看代表状态的入度。

因此:

  • 从横向看,能看到某个当前状态可以转入哪些目标状态。
  • 从纵向看,能看到哪些当前状态可以转入某个目标状态。

在代码里,矩阵可以这样表示:

js 复制代码
const states = ["A", "B", "C", "D"];

const transitionMatrix = [
  [0, 1, 1, 1],
  [1, 0, 1, 1],
  [1, 1, 0, 1],
  [1, 1, 1, 0],
];

为了便于查询,需要建立状态和矩阵下标之间的映射:

js 复制代码
const stateIndex = {
  A: 0,
  B: 1,
  C: 2,
  D: 3,
};

这样判断 A 是否可以转换成 B 时,只需要读取:

js 复制代码
transitionMatrix[stateIndex.A][stateIndex.B] === 1;

如果状态数量不多、规则固定,邻接矩阵非常直观。它的优点是判断速度快,结构清晰,适合表达"任意两个状态之间是否允许转换"的关系。

如果状态数量较多,或者每个状态只允许转换到少量状态,也可以使用邻接表:

js 复制代码
const transitions = {
  A: ["B", "C", "D"],
  B: ["A", "C", "D"],
  C: ["A", "B", "D"],
  D: ["A", "B", "C"],
};

邻接表更节省空间,也更接近业务语义。它表达的是:每个状态后面列出的数组,就是该状态允许转换到的目标状态。

第三步:实现状态机

状态机实现时,核心入口通常只需要两个能力:

  1. 判断能否转换:canTransition(from, to)
  2. 执行状态变更:transition(entity, to)

一个基于邻接矩阵的基础实现如下:

js 复制代码
class StateMachine {
  constructor(states, transitionMatrix) {
    this.states = states;
    this.transitionMatrix = transitionMatrix;
    this.stateIndex = states.reduce((indexMap, state, index) => {
      indexMap[state] = index;
      return indexMap;
    }, {});
  }

  hasState(state) {
    return Object.prototype.hasOwnProperty.call(this.stateIndex, state);
  }

  canTransition(from, to) {
    if (!this.hasState(from) || !this.hasState(to)) {
      return false;
    }

    const fromIndex = this.stateIndex[from];
    const toIndex = this.stateIndex[to];

    return this.transitionMatrix[fromIndex][toIndex] === 1;
  }

  transition(entity, to) {
    const from = entity.status;

    if (!this.canTransition(from, to)) {
      throw new Error(`invalid state transition: ${from} -> ${to}`);
    }

    entity.status = to;
    return entity;
  }
}

使用方式:

js 复制代码
const machine = new StateMachine(states, transitionMatrix);

const order = {
  id: 1,
  status: "A",
};

machine.transition(order, "B");

console.log(order.status); // B

这个实现的关键点在于,业务对象本身只保存当前状态,例如 entity.status。真正的状态规则不散落在业务代码里,而是由状态机统一维护。业务代码只提出"我要从当前状态变更到目标状态",状态机负责判断这件事是否允许发生。

在真实业务中,状态变更通常还会包含更多逻辑,例如:

  • 变更前校验:检查权限、数据完整性、前置条件。
  • 变更中处理:更新数据库中的状态字段。
  • 变更后动作:记录日志、发送通知、触发事件。

可以在基础状态机上继续扩展钩子:

js 复制代码
class StateMachineWithHooks extends StateMachine {
  constructor(states, transitionMatrix, hooks = {}) {
    super(states, transitionMatrix);
    this.hooks = hooks;
  }

  transition(entity, to, context = {}) {
    const from = entity.status;

    if (!this.canTransition(from, to)) {
      throw new Error(`invalid state transition: ${from} -> ${to}`);
    }

    const transitionKey = `${from}->${to}`;
    const hook = this.hooks[transitionKey];

    if (hook?.before) {
      hook.before(entity, context);
    }

    entity.status = to;

    if (hook?.after) {
      hook.after(entity, context);
    }

    return entity;
  }
}

示例:

js 复制代码
const machine = new StateMachineWithHooks(states, transitionMatrix, {
  "A->B": {
    before(entity) {
      if (!entity.id) {
        throw new Error("entity id is required");
      }
    },
    after(entity) {
      console.log(`entity ${entity.id} changed to ${entity.status}`);
    },
  },
});

这样,状态机就从一个单纯的合法性判断器,变成了状态流转的统一执行入口。

总结来看,状态机的核心实现路径是:

  1. 先设计状态和流转规则,把状态关系抽象成有向图。
  2. 再选择合适的数据结构,使用邻接矩阵或邻接表描述状态之间的边。
  3. 最后提供统一的转换入口,在入口中完成合法性判断、状态变更和扩展动作。

只要所有状态变更都经过这个入口,状态机就能保证系统中的状态始终按照预先定义好的规则流转。

相关推荐
星栈1 小时前
LiveView 的生命周期:mount、handle_event 和 Socket 到底怎么运转
前端·前端框架·elixir
yingyima1 小时前
JWT Token 解析与安全实践速查:5 问 5 答直击要害
前端
kyriewen2 小时前
我用 Codex 重写了同事维护三年的代码,他没说谢谢——而是找了领导
前端·javascript·ai编程
OpenTiny社区2 小时前
从零开发 AI 聊天页要两周?试试这款 Vue3 垂直对话组件库 TinyRobot,直接开箱即用
前端·vue.js·github
铁皮饭盒3 小时前
S3已成为文件存储标准,阿里/腾讯/华为云都支持,Bun率先原生支持
前端·javascript·后端
Cobyte3 小时前
22.Vue Vapor 组件 props 的实现
前端·javascript·vue.js
lichenyang4533 小时前
从 has.showToast 看 ASCF 的 API 调用链路
前端
张就是我1065924 小时前
DOMPurify 的一个漏洞:你以为 {} 是空的?
前端
疯狂的魔鬼5 小时前
一套 Schema 驱动四视图:记 useCrudSchemas 的设计与实践
前端·javascript·typescript