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。
重构的核心思路:状态模式 + 状态表
重构复杂状态机,我个人习惯用两个工具:状态模式和状态转换表。
状态模式,就是把每个状态的行为封装成一个独立的类。状态转换表,则是用一张表来定义“当前状态 + 触发事件 -> 下一个状态 + 执行动作”。
说白了,就是把“状态判断”和“业务逻辑”彻底分开。
第一步:定义状态和事件
先别急着写代码。把所有的状态和事件列出来,画一张图。
这张图一画出来,状态流转就一目了然了。接下来,我们把这张图翻译成代码。
第二步:用状态表驱动
我个人习惯先定义一张状态转换表。这张表就是整个状态机的“宪法”。
// 状态转换表
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)
避坑指南
我在实践中踩过不少坑,分享几个给你:
- 不要滥用状态机。如果状态少于 3 个,或者状态流转很简单,用 if-else 反而更直接。状态机适合状态多、流转复杂的场景。
- 动作方法要保持单一职责。一个动作只做一件事。比如 doPay 只处理支付逻辑,不要在里面顺便发邮件、写日志。那些可以放在 onTransition 回调里做。
- 状态表要支持扩展。我见过有人把状态表写死在代码里,后来业务要动态调整状态流转,改起来很痛苦。建议把状态表做成可配置的。
- 注意并发问题。如果状态机在分布式环境下使用,要考虑状态更新的原子性。可以用数据库乐观锁或分布式锁来保证。
总结
重构复杂状态机逻辑,说白了就是三件事:
- 画图:把状态流转画出来,让所有人看得懂
- 建表:用状态转换表定义流转规则
- 解耦:用状态模式把状态判断和业务逻辑分开
这样做的好处很明显:代码可读性高了,维护成本低了,加新状态也不怕了。你想想看,下次产品经理说“我们要加一个‘待评价’状态”,你只需要在状态表里加一行,再写一个 doEvaluate 方法。是不是轻松多了?
重构不是一蹴而就的。如果你现在的状态机代码已经乱成一团,别急着全部重写。可以先从最核心的状态流转开始,逐步替换。每次重构一小块,保证系统稳定运行。这才是重构的正确姿势。
公众号:蓝海资料掘金营,微信deep3321