状态机设计理念与实现

状态机设计理念与实现

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

  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. 最后提供统一的转换入口,在入口中完成合法性判断、状态变更和扩展动作。

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

相关推荐
乱码三千7 分钟前
密码管理工具站正式上线啦
前端·html·github
IMPYLH12 分钟前
HTML 的 <rt> 元素
前端·html
单线程_0118 分钟前
从案例分析 Vue3 Tokenizer+Parser 源码五
前端·javascript·vue.js
mldong8 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排8 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60619 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角10 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!10 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰10 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi12 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js