77、重构与状态管理:重构复杂的状态机逻辑

状态机,说白了就是让代码“记住自己到哪了”。

我见过太多项目,一开始状态少,用几个 if-else 就搞定了。等业务复杂起来,状态一多,代码就成了一团乱麻。你想想看,一个订单状态流转,从“待支付”到“已支付”再到“已发货”,中间还可能穿插“退款中”、“已取消”…… 如果全靠 if-else 堆,维护起来简直是噩梦。

今天我们就聊聊,怎么把这种复杂的状态机逻辑,重构得清晰、可维护。

为什么状态机容易变烂?

核心原因就一个:状态和行为的耦合

很多同学写代码时,习惯把状态判断和业务逻辑混在一起。比如:

// 糟糕的写法
if (order.status === 'pending') {
    if (action === 'pay') {
        // 执行支付逻辑
        order.status = 'paid';
    } else if (action === 'cancel') {
        // 执行取消逻辑
        order.status = 'cancelled';
    }
} else if (order.status === 'paid') {
    if (action === 'ship') {
        // 执行发货逻辑
        order.status = 'shipped';
    }
}

这种代码,状态一多,if 嵌套就深得吓人。我在项目中遇到过,一个订单状态流转的代码,光 if-else 就写了 200 多行。后来加一个新状态,改了一个地方,结果三个地方跟着出 bug。

我曾经接手过一个项目,状态机逻辑散落在十几个文件里。每次改需求,都要全局搜索“status”关键字,然后一个个排查。那感觉,就像在雷区里跳舞。

重构的核心思路:状态模式 + 状态表

重构复杂状态机,我个人习惯用两个工具:状态模式状态转换表

状态模式,就是把每个状态的行为封装成一个独立的类。状态转换表,则是用一张表来定义“当前状态 + 触发事件 -> 下一个状态 + 执行动作”。

说白了,就是把“状态判断”和“业务逻辑”彻底分开。

第一步:定义状态和事件

先别急着写代码。把所有的状态和事件列出来,画一张图。

订单状态机流转图 待支付 已支付 已发货 已取消 退款中 支付 发货 取消 取消 退款

这张图一画出来,状态流转就一目了然了。接下来,我们把这张图翻译成代码。

第二步:用状态表驱动

我个人习惯先定义一张状态转换表。这张表就是整个状态机的“宪法”。

// 状态转换表
const stateMachine = {
    'pending': {
        'pay': { to: 'paid', action: 'doPay' },
        'cancel': { to: 'cancelled', action: 'doCancel' }
    },
    'paid': {
        'ship': { to: 'shipped', action: 'doShip' },
        'cancel': { to: 'cancelled', action: 'doRefund' }
    },
    'shipped': {
        'refund': { to: 'refunding', action: 'doRefundRequest' }
    },
    'refunding': {
        'approve': { to: 'cancelled', action: 'doRefundApprove' },
        'reject': { to: 'shipped', action: 'doRefundReject' }
    }
};

你看,这张表把“当前状态”、“触发事件”、“下一个状态”、“要执行的动作”都定义清楚了。代码里再也不需要 if-else 了。

小技巧:状态表可以放在配置文件里,甚至可以从数据库读取。这样,业务人员改状态流转时,开发人员只需要改配置,不用改代码。

第三步:实现状态机引擎

有了状态表,我们再写一个通用的状态机引擎。这个引擎负责:

  • 根据当前状态和事件,查表找到下一个状态和动作
  • 执行动作
  • 更新状态
  • 触发回调(比如记录日志、发送通知)
class StateMachine {
    constructor(table, initialState) {
        this.table = table;
        this.state = initialState;
        this.listeners = [];
    }

    // 触发事件
    dispatch(event, payload) {
        const currentState = this.state;
        const transition = this.table[currentState]?.[event];

        if (!transition) {
            throw new Error(`状态 ${currentState} 不允许事件 ${event}`);
        }

        // 执行动作
        if (transition.action) {
            this[transition.action]?.(payload);
        }

        // 更新状态
        this.state = transition.to;

        // 触发回调
        this.listeners.forEach(fn => fn({
            from: currentState,
            to: this.state,
            event,
            payload
        }));
    }

    // 注册状态变更监听
    onTransition(fn) {
        this.listeners.push(fn);
    }

    // 具体的业务动作
    doPay(payload) {
        console.log('执行支付逻辑', payload);
    }

    doCancel(payload) {
        console.log('执行取消逻辑', payload);
    }

    doShip(payload) {
        console.log('执行发货逻辑', payload);
    }

    doRefund(payload) {
        console.log('执行退款逻辑', payload);
    }

    doRefundRequest(payload) {
        console.log('发起退款申请', payload);
    }

    doRefundApprove(payload) {
        console.log('退款审批通过', payload);
    }

    doRefundReject(payload) {
        console.log('退款审批拒绝', payload);
    }
}

嗯,这里要注意:动作方法可以单独抽出来,不要全写在一个类里。如果动作逻辑复杂,可以拆成独立的 Service 或 Handler。

第四步:使用状态机

使用起来就很简单了:

// 创建状态机实例
const orderMachine = new StateMachine(stateMachine, 'pending');

// 监听状态变化
orderMachine.onTransition(({ from, to, event }) => {
    console.log(`订单状态: ${from} -> ${to} (触发事件: ${event})`);
    // 这里可以记录日志、发送通知等
});

// 触发事件
orderMachine.dispatch('pay', { orderId: 123, amount: 99.9 });
// 输出: 订单状态: pending -> paid (触发事件: pay)

orderMachine.dispatch('ship', { orderId: 123, trackingNo: 'SF123456' });
// 输出: 订单状态: paid -> shipped (触发事件: ship)
核心变化:重构前,状态判断散落在各处,改一个状态要改 N 个文件。重构后,状态流转集中在一张表里,业务逻辑封装在动作方法里。加一个新状态,只需要在表里加一行,再实现对应的动作方法即可。

避坑指南

我在实践中踩过不少坑,分享几个给你:

  • 不要滥用状态机。如果状态少于 3 个,或者状态流转很简单,用 if-else 反而更直接。状态机适合状态多、流转复杂的场景。
  • 动作方法要保持单一职责。一个动作只做一件事。比如 doPay 只处理支付逻辑,不要在里面顺便发邮件、写日志。那些可以放在 onTransition 回调里做。
  • 状态表要支持扩展。我见过有人把状态表写死在代码里,后来业务要动态调整状态流转,改起来很痛苦。建议把状态表做成可配置的。
  • 注意并发问题。如果状态机在分布式环境下使用,要考虑状态更新的原子性。可以用数据库乐观锁或分布式锁来保证。
我曾经在一个项目中,状态机没有做并发控制。结果两个请求同时触发状态变更,导致订单状态跳过了“已支付”直接到了“已发货”。那一次,我们花了整整两天排查问题。

总结

重构复杂状态机逻辑,说白了就是三件事:

  1. 画图:把状态流转画出来,让所有人看得懂
  2. 建表:用状态转换表定义流转规则
  3. 解耦:用状态模式把状态判断和业务逻辑分开

这样做的好处很明显:代码可读性高了,维护成本低了,加新状态也不怕了。你想想看,下次产品经理说“我们要加一个‘待评价’状态”,你只需要在状态表里加一行,再写一个 doEvaluate 方法。是不是轻松多了?

重构不是一蹴而就的。如果你现在的状态机代码已经乱成一团,别急着全部重写。可以先从最核心的状态流转开始,逐步替换。每次重构一小块,保证系统稳定运行。这才是重构的正确姿势。


公众号:蓝海资料掘金营,微信deep3321