别再写 if-else 了:状态机才是复杂业务逻辑的正确答案
你有没有这种感觉:代码写着写着,if-else 越来越多,多到自己都不想看,多到改一个小功能要小心翼翼,多到 Bug 永远修不完?
恭喜你,你已经陷入了「条件地狱」。
一个真实的噩梦
先说个真实故事。我接手过一个订单系统,原来的开发者在订单状态处理上写了将近 2000 行 switch-case,每增加一个新状态就要改三个地方,测试要回归 40 多个场景。那代码看起来像是被卡车碾过的意面。
你以为加注释就能解决?不,注释只会让代码看起来更加混乱。
我当时的第一反应是:重构。但仔细一看,2000 行代码全是一个模式——根据当前状态 + 事件,决定下一步该干什么。这不就是一个状态机吗?
状态机是什么?
状态机,全称有限状态机(Finite State Machine),本质上就是描述「在什么状态下,响应什么事件,会转移到什么新状态,同时执行什么动作」。
听起来很简单对吧?但就是这么简单的东西,能救你一命。
用一个最常见的例子——订单状态的流转:
待支付 → 已支付 → 已发货 → 已收货 → 已完成
↓ ↓ ↓ ↓
[超时取消] [退款申请] [退货申请] [申诉中]
如果用 if-else 写,这段逻辑会写成什么样?
function handleOrderEvent(order, event) {
if (order.status === "PENDING" && event === "PAY") {
order.status = "PAID";
sendNotification();
} else if (order.status === "PENDING" && event === "TIMEOUT") {
order.status = "CANCELLED";
refund();
} else if (order.status === "PAID" && event === "SHIP") {
order.status = "SHIPPED";
updateLogistics();
} else if (order.status === "PAID" && event === "REFUND_REQUEST") {
order.status = "REFUND_PENDING";
notifyMerchant();
}
// ... 你知道会变成什么样
}
然后某天产品说:「已发货状态下也要能申请退款」。你开始加 if-else,然后发现要改 7 个地方。
然后某天运营说:「部分退款也要考虑」。你看着那 2000 行代码陷入了沉默。
状态机的优雅实现
状态机的核心是把「状态」和「转移规则」分离,让规则变成数据而不是代码。
const transitions = {
PENDING: {
PAY: { nextState: "PAID", action: "sendNotification" },
TIMEOUT: { nextState: "CANCELLED", action: "refund" },
CANCEL: { nextState: "CANCELLED", action: null }
},
PAID: {
SHIP: { nextState: "SHIPPED", action: "updateLogistics" },
REFUND_REQUEST: { nextState: "REFUND_PENDING", action: "notifyMerchant" }
},
SHIPPED: {
RECEIVE: { nextState: "COMPLETED", action: "confirmDelivery" },
REFUND_REQUEST: { nextState: "REFUND_PENDING", action: "notifyMerchant" }
},
REFUND_PENDING: {
APPROVE: { nextState: "REFUNDED", action: "processRefund" },
REJECT: { nextState: "SHIPPED", action: "notifyCustomer" }
}
};
function handleOrderEvent(order, event) {
const currentTransitions = transitions[order.status];
if (!currentTransitions) {
throw new Error(`未知状态: ${order.status}`);
}
const transition = currentTransitions[event];
if (!transition) {
throw new Error(`状态 ${order.status} 下不支持事件: ${event}`);
}
order.status = transition.nextState;
if (transition.action) {
executeAction(transition.action, order);
}
}
现在,加一个新状态?加一个新的状态节点定义就行了。加一个新事件?加一条转移规则就行了。再也不用改那个巨大的 if-else 链了。
状态机的高级玩法
1. 守卫条件(Guard Conditions)
有时候同一个状态 + 同一个事件,不同条件下要转移到不同状态。比如:
PAID: {
REFUND_REQUEST: [
{
condition: (order) => order.amount > 1000,
nextState: "MANUAL_REVIEW",
action: "assignReviewer"
},
{
condition: (order) => order.amount <= 1000,
nextState: "REFUND_PENDING",
action: "autoApprove"
}
]
}
大额退款需要人工审核,小额自动通过——这就是守卫条件。
2. 动作执行时机
很多人在状态机里纠结动作该在哪里执行。我的经验是:
- 进入动作(Entry Action):每次进入状态时执行,适合初始化、通知等
- 退出动作(Exit Action):每次离开状态时执行,适合资源清理等
- 转移动作(Transition Action):转移过程中执行,适合记录日志、发送事件等
建议把动作执行和状态转移解耦,用事件总线来处理副作用。状态机只负责状态流转,副作用通过发布事件来触发。这样测试也好写,调试也清晰。
3. 状态机可视化
用 JSON 定义状态机之后,可以直接生成状态图。推荐用 xstate 的可视化工具,或者自己写个简单的 DOT 语言导出。好处是:产品、设计、测试都能看懂,大家对业务逻辑的理解能对齐。
什么时候不该用状态机?
状态机不是银弹。我见过有人用状态机处理用户输入验证,那纯属杀鸡用牛刀。
简单判断标准:
- 状态种类 ≤ 3 种,事件类型 ≤ 5 种?用 if-else 就够了,别折腾
- 状态组合会指数增长?状态机会爆炸,考虑用决策树或规则引擎
- 需要并行状态?普通状态机hold不住,考虑层级状态机(Hierarchical State Machine)
状态机最适合的场景是:状态种类明确、事件类型明确、状态转移规则是业务核心的场景。电商订单、工作流审批、游戏角色状态、设备状态管理——这些用状态机就是降维打击。
吐槽时间
我发现很多程序员有个毛病:喜欢用复杂的技巧来展示自己有多厉害,而不是用最简单清晰的方案来解决问题。if-else 写 2000 行,看着是「业务能力强」,实际上是不动脑子。
另一个极端是「过度设计」,一个简单的订单状态流转非要上 xstate + 类型安全的状态机定义,项目复杂度直接起飞。
我的建议是:在写代码之前,先想清楚业务的本质模型。状态机不是炫技,是把混乱的业务逻辑用一种清晰的模型来表达。模型选对了,代码自然就简单了。
下次当你发现自己在写第六层嵌套 if-else 的时候,停一下,问问自己:这个东西的本质是什么?也许你需要的不是更多条件,而是一个状态机。
最后送大家一句话:代码是写给人看的,顺手给机器执行。if-else 写多了,机器看得懂,人看不懂,这样的代码迟早要还的。