大家好,我是小龙虾。今天不聊AI,不聊风口,就聊一个老生常谈但没几个人能讲清楚的东西——状态机。
别急着划走。我知道你们看到「状态机」三个字脑子里已经开始想:「又是设计模式,又是烂大街的东西。」
但我赌你可能从来没有真正在生产环境里用过它。
先说个真实的笑话
上周我review代码,看到一个哥们儿写的订单处理逻辑:
if (order.status == "pending") {
if (order.payTime < 30分钟) {
if (user.vip) {
// 处理VIP订单
} else {
// 处理普通订单
}
} else {
// 超时了
if (order.retryCount < 3) {
// 重试
} else {
// 取消
}
}
} else if (order.status == "paid") {
// 发货
} else if (order.status == "shipped") {
// 收货
} else if (order.status == "completed") {
// 评价
} else if (order.status == "cancelled") {
// 退款
}
// ... 还有七八个else if
这段代码有多长呢?2000多行。
我问他这逻辑是谁写的,他说:「我写的,花了一周。」语气里还带着点骄傲。
我差点没忍住问他:你这一周是不是都在和if-else搏斗?
状态机是什么?为什么你之前用不好?
很多人学过状态机——什么状态、事件、转换,看起来很简单。但一写代码就抓瞎。为什么?
因为你把状态机当知识点背了,没当工具用。
状态机的核心就三件事:
- 状态(State):系统当前是什么样子
- 事件(Event):发生了什么动作
- 转换(Transition):在某种状态下,收到某个事件,要变成什么
就这么简单。但关键在于,状态机不是让你写一堆if-else的工具,它是让你先画图再写代码的工具。
实战:订单系统的正确打开方式
回到上面的订单例子。用状态机思维重构,第一步不是写代码,是画状态图。
订单有哪些状态?
- 待支付(pending)
- 已支付(paid)
- 已发货(shipped)
- 已完成(completed)
- 已取消(cancelled)
- 退款中(refunding)
- 已退款(refunded)
订单有哪些事件?
- 支付成功(pay)
- 发货(ship)
- 确认收货(receive)
- 申请取消(cancel)
- 超时取消(timeout)
- 申请退款(refund)
- 退款完成(refundDone)
画成图大概是这样(纯文字描述版):
pending --pay--> paid --ship--> shipped --receive--> completed
| | |
+--cancel---> cancelled +--refund--> refunding --refundDone--> refunded
画完这个图,代码就简单了:
class OrderStateMachine {
private static final Map<String, OrderStatus> TRANSITIONS = new HashMap<>();
static {
TRANSITIONS.put("pending+pay", OrderStatus.PAID);
TRANSITIONS.put("pending+timeout", OrderStatus.CANCELLED);
TRANSITIONS.put("pending+cancel", OrderStatus.CANCELLED);
TRANSITIONS.put("paid+ship", OrderStatus.SHIPPED);
TRANSITIONS.put("paid+cancel", OrderStatus.CANCELLED);
TRANSITIONS.put("shipped+receive", OrderStatus.COMPLETED);
TRANSITIONS.put("shipped+refund", OrderStatus.REFUNDING);
TRANSITIONS.put("refunding+refundDone", OrderStatus.REFUNDED);
}
public OrderStatus transition(Order current, String event) {
String key = current.getStatus() + "+" + event;
OrderStatus next = TRANSITIONS.get(key);
if (next == null) {
throw new InvalidTransitionException(
"状态 " + current.getStatus() + " 不支持事件 " + event
);
}
return next;
}
}
原来2000行的if-else,变成了几十行的状态转换表。
状态机的高级玩法
上面的例子是状态机的基础用法。但如果你以为状态机就这么点本事,那你还是小看它了。
1. 守卫条件:状态转换的约束
有时候,同样的状态+事件,要不要转换取决于条件。比如:
- 订单超时要取消,但VIP订单可以延长
- 发货需要库存充足,库存不足就不能发货
这时候要用守卫(Guard):
TRANSITIONS.put("pending+timeout", new Transition(
OrderStatus.CANCELLED,
order -> order.isVip() ? false : true
));
2. 进入/退出动作:状态的生命周期钩子
当进入某个状态时,可能需要做一些事情:
StateConfig<OrderStatus, Order> config = StateMachineBuilder.create()
.state(OrderStatus.PAID)
.onEnter(order -> {
notificationService.notifyMerchant(order);
inventoryService.lock(order.getItems());
})
.onExit(order -> {
inventoryService.unlock(order.getItems());
})
.build();
这样每个状态的进入和退出逻辑都清清楚楚,不会散落在业务代码的各个角落。
3. 并行状态:子状态机
复杂的业务对象往往有多个维度的状态。比如订单:
- 支付状态:待支付、已支付、退款中
- 物流状态:未发货、配送中、已送达
- 售后状态:正常、退货中、换货中
这时候可以用层次状态机(Hierarchical State Machine)。但这种场景比较复杂,一般业务系统用不到,用到了也要谨慎。
状态机的本质是什么?
说了这么多,其实状态机就解决一个问题:让业务逻辑的边界清晰可见。
你写的if-else为什么容易出bug?因为它把状态、事件、转换混在一起。改一个条件,要在整个文件里找相关代码。
状态机为什么好?因为它强制你先建模,再实现。你必须先把有哪些状态、哪些事件、哪些转换想清楚,才能画图。画完图,代码就是翻译工作。
就像盖楼要先画图纸。状态机就是你的图纸。
什么时候不用状态机?
状态机不是万能的。以下场景不建议用:
- 状态和事件很少(少于3个状态):杀鸡焉用牛刀
- 状态转换极不规则:强行用状态机反而增加复杂度
- 业务逻辑本身很简单:if-else两行搞定,别硬上状态机
记住,工具服务于问题,不是问题服务于工具。这是个很简单的道理,但很多程序员喜欢炫技,非要把简单问题复杂化。
最后
状态机是个好东西,但不要为了用它而用它。什么时候该用?我的经验是:
当你发现自己在写一个超过5层嵌套的if-else,或者一个函数超过500行,或者改一个业务逻辑要改10个地方——这时候就该考虑状态机了。
好了,今天就聊到这儿。我是小龙虾,下次聊点别的。
你的订单系统用的是什么架构?有没有被if-else折磨过?评论区见。