订单状态的 if-else 地狱上线就崩——状态模式的工业级落地
订单状态的 if-else 地狱上线就崩——状态模式的工业级落地
电商系统的订单状态流转,大概是 if-else 癌变的重灾区。
我见过的「杰出」代表:
java public void processOrder(Order order, String action) { if ("PENDING".equals(order.getStatus())) { if ("pay".equals(action)) { order.setStatus("PAID"); inventoryService.deduct(order); notificationService.sendPaidNotice(order); } else if ("cancel".equals(action)) { order.setStatus("CANCELLED"); inventoryService.release(order); } else { throw new IllegalStateException("待支付状态下不允许" + action); } } else if ("PAID".equals(order.getStatus())) { if ("ship".equals(action)) { order.setStatus("SHIPPED"); logisticsService.createWaybill(order); notificationService.sendShippedNotice(order); } else if ("refund".equals(action)) { order.setStatus("REFUNDING"); paymentService.refund(order); } else { throw new IllegalStateException("已支付状态下不允许" + action); } } else if ("SHIPPED".equals(order.getStatus())) { // ... 状态越来越多,if-else 越来越深 } }
这个方法的作者后来离职了。他留下的遗产是 500 多行的processOrder,包含 12 种状态、45 种状态转换。每加一个新状态,要找遍所有 if-else 分支里有没有遗漏。改一行就怕改错十行,上线一次心惊肉跳一次。
状态机最怕的不是复杂,是「看起来简单」
很多人抗拒状态模式的原因是:我的状态就 3、4 个,用 if-else 够了。
说这话的人通常没意识到两件事:
第一,状态不是数量问题,是转换规则分散的问题。3 个状态 A→B→C,看起来简单。但以后加一个「异常状态」,你要在原有的 3 个 if 分支里各加一条判断。这种改动是横切式的——你改的不是一个新方法,而是在几十行已有的代码里插针。
第二,if-else 状态机的 Bug 不会在第一个月暴露。因为第一个月只有正常流程。第二个月产品说「加上退款」,第三个月运营说「加上部分退款」,第四个月财务说「加上发票状态联动」——每次改动都在原有的 if-else 上打补丁,半年后没人能说清楚一个订单到底有多少种合法状态。
状态模式的核心不是代码换位置,是职责归位
java // 状态接口 public interface OrderState { OrderState pay(Order order); OrderState ship(Order order); OrderState cancel(Order order); OrderState refund(Order order); }
每个具体状态实现自己的转换规则:
```java public class PendingState implements OrderState { @Override public OrderState pay(Order order) { order.setPaidTime(System.currentTimeMillis()); return new PaidState(); }
@Override public OrderState cancel(Order order) { return new CancelledState(); } @Override public OrderState ship(Order order) { throw new IllegalStateException("待支付状态不能发货"); } @Override public OrderState refund(Order order) { throw new IllegalStateException("待支付状态不能退款"); }} ```
状态模式把「这个状态下能做什么」和「谁来做这个判断」画了清晰的边界。
加一个新状态——比如PartialRefundState(部分退款)——只需要新建一个类,实现接口,不影响现有的任何状态类。这个改动是纵向的——只影响新的代码,不破坏旧的代码。
状态的物理存储是个真坑
状态模式最大的工程坑不在设计,在持久化。
```java // 你的状态对象在内存里 OrderState currentState = new PaidState();
// 但数据库里只有一个 status 字段 //statusvarchar(20) NOT NULL DEFAULT 'PENDING' ```
状态模式是面向对象的,状态是个对象,有行为。但数据库是面向数据的,状态是个字符串,只有值。两者之间必须有一个映射层:
```java public class OrderStateFactory { private static final Map STATE_MAP = new HashMap<>();
static { STATE_MAP.put("PENDING", new PendingState()); STATE_MAP.put("PAID", new PaidState()); STATE_MAP.put("SHIPPED", new ShippedState()); STATE_MAP.put("COMPLETED", new CompletedState()); STATE_MAP.put("CANCELLED", new CancelledState()); STATE_MAP.put("REFUNDING", new RefundingState()); } public static OrderState fromCode(String statusCode) { OrderState state = STATE_MAP.get(statusCode); if (state == null) { throw new IllegalArgumentException("未知订单状态: " + statusCode); } return state; }} ```
这里有个隐蔽的并发问题:状态对象是无状态的,所以可以用单例。但如果你的状态对象里存了临时数据(比如paidTime),就不能用单例——每次要从数据库重建。这违背了状态模式「状态决定行为」的初衷。
正确的做法:行为在状态对象里,数据在 Order 实体里。
java // PendingState 只管「能做什么、不能做什么」 // 数据(paidTime、cancelReason)全在 Order 对象里 // 状态对象不存任何业务数据
状态爆炸——状态模式自己挖的坑
状态模式消除条件分支的代价是类爆炸。一个订单系统如果真的有 12 种状态,你就需要 12 个状态类,每个类实现 6-8 个方法(每种可能操作一个方法)。
有些操作在大部分状态下是合法的(比如「查看订单」),但在某些状态下不合法。你需要在每个状态类里写这些方法的实现——即使大部分实现是相同的。
解决思路:抽象基类提供默认行为,子类只覆盖差异。
```java public abstract class AbstractOrderState implements OrderState { @Override public OrderState ship(Order order) { throw new IllegalStateException("当前状态不支持发货"); }
@Override public OrderState refund(Order order) { throw new IllegalStateException("当前状态不支持退款"); } // 公共操作:所有状态都允许查询 public Order query(Order order) { return order; }} ```
然后用一个状态转换表来管理所有合法转换,避免在每个状态类里硬编码:
```java public class OrderStateMachine { private static final Set ALLOWED = new HashSet<>();
static { ALLOWED.add(new Transition("PENDING", "pay", "PAID")); ALLOWED.add(new Transition("PENDING", "cancel", "CANCELLED")); ALLOWED.add(new Transition("PAID", "ship", "SHIPPED")); ALLOWED.add(new Transition("PAID", "refund", "REFUNDING")); ALLOWED.add(new Transition("SHIPPED", "confirm", "COMPLETED")); // ... 所有合法转换 } public static boolean isAllowed(String from, String action, String to) { return ALLOWED.contains(new Transition(from, action, to)); }} ```
转换表和状态类配合使用:状态类负责行为逻辑(发货=创建物流单+发通知),转换表负责合法性校验。
什么时候不该用状态模式
不是所有 if-else 都需要用状态模式消灭。
不该用的场景:- 状态不超过 3 个,且未来几乎不会增加——引入 3 个类 + 1 个接口不值得 - 状态转换非常线性(A→B→C→结束),没有分支——if-else 可读性更好 - 每种状态的行为差异极小(只有一个字段不同)——策略模式更合适
该用的场景:- 状态超过 5 个,且每个状态有 3+ 个不同行为 - 状态会持续增加(迭代中需求频繁加新状态) - 不同状态的行为逻辑差异大(不是一个if status==X能覆盖的) - 多人维护同一段状态流转代码——减少合并冲突
判断标准不是「我的状态多不多」,而是「改状态流转的时候,要不要动到已有的代码」。如果要,就该重构。
真实案例:退款流程的状态爆炸
去年做的一个退款模块,产品文档写了 7 种退款状态:
待审核 → 审核通过 → 退款中 → 退款成功 ↓ 审核拒绝 ↓ 退款中 → 退款失败 → 人工处理
看起来只有 7 个状态。但实际开发中发现:退款失败后重试,可能回到「退款中」;人工处理后可能直接「退款成功」或「关闭退款」;「审核拒绝」后用户可以重新提交,回到「待审核」。
最终状态转换图有 14 条边,7 个状态类。如果不做状态模式而是用 if-else,这段代码的圈复杂度会超过 30——SonarQube 直接标红。
写成状态模式之后,每个状态类平均 30-50 行,单元测试覆盖了所有转换路径。唯一多出来的是 7 个状态类的样板代码,但换来了「加一个新状态不改老代码」的安全感。
