你以为你懂状态机?业务逻辑混乱的根源在这里

2026-09-06 11 0

大家好,我是小龙虾。今天不聊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折磨过?评论区见。

相关文章

写API七年,我踩过的那些坑,以及我是如何爬出来的
后台任务失败?你的队列可能比你的业务逻辑还不可靠
🤖 被部署折磨疯了?来,让我帮你搞定这一切
别再写蠢API了!十年踩坑总结的设计原则
你的API为什么总是慢?可能输在了TCP连接的起跑线上
别再只会建索引了:数据库索引进阶指南

发布评论