订单状态一旦扩展到取消、支付、发货、退款和售后,散落在服务层的 if-else 很快会变成难以修改的业务迷宫。本文以订单状态流转为例,分析条件分支失控的根因,并用状态模式重构一套职责清晰、便于测试和扩展的 Java 代码。

问题背景:状态不是一个字段,而是一组约束

很多业务对象都有状态:订单有待支付、已支付、已发货,工单有待处理、处理中、已完成,账户有正常、冻结、注销。最初的实现往往很简单,在一个 Service 里写几个判断:

public void changeStatus(Long orderId, String action) {
    Order order = orderRepository.findById(orderId);

    if ("pay".equals(action)) {
        if (!"CREATED".equals(order.getStatus())) {
            throw new IllegalStateException("只有待支付订单可以支付");
        }
        order.setStatus("PAID");
    } else if ("ship".equals(action)) {
        if (!"PAID".equals(order.getStatus())) {
            throw new IllegalStateException("只有已支付订单可以发货");
        }
        order.setStatus("SHIPPED");
    } else if ("cancel".equals(action)) {
        if (!"CREATED".equals(order.getStatus())
                && !"PAID".equals(order.getStatus())) {
            throw new IllegalStateException("当前状态不能取消");
        }
        order.setStatus("CANCELLED");
    }

    orderRepository.save(order);
}

这段代码在只有三个动作时并不糟糕。真正的问题出现在需求继续增长之后:支付成功要记录时间,发货前要检查库存,取消要根据付款状态触发退款,退款又会引入新的状态。状态判断、业务动作、异常提示和副作用逐渐混在一起。

此时,代码的复杂度并不只来自分支数量,而来自状态与动作之间的组合关系。一个状态新增一条规则,可能需要修改多个方法;一个动作增加一个前置条件,也可能影响其他流程。修改成本和回归风险一起上升。

先明确核心原则:状态决定允许做什么

状态模式适合解决这样的问题:对象的行为会随着内部状态改变,并且不同状态下的行为差异较大。

在订单场景中,可以把问题拆成三层:

  1. Order 负责保存订单当前状态,以及对外提供稳定的业务操作。
  2. OrderState 表示某个状态下允许执行的动作。
  3. 具体状态类负责实现该状态下的规则,例如待支付订单允许支付和取消,但不允许发货。

这样做不是为了把一个类拆成很多类,而是为了让变化集中。状态相关的规则放在状态对象中,订单本身不需要知道每一种状态的细节。

第一步:用枚举表达持久化状态

数据库中的状态通常仍然适合使用枚举对应的字符串或数字。枚举可以避免业务代码到处出现魔法字符串:

public enum OrderStatus {
    CREATED,
    PAID,
    SHIPPED,
    COMPLETED,
    CANCELLED,
    REFUNDED
}

这里的枚举只描述状态集合,不直接承担全部流转逻辑。状态值需要持久化,因此新增枚举值时还要同步考虑数据库兼容性、历史数据和查询条件,不能只把它当作普通常量。

第二步:定义状态行为接口

状态接口只声明订单真正需要的动作。示例中保留支付、发货、完成和取消四个动作:

public interface OrderState {

    void pay(OrderContext context);

    void ship(OrderContext context);

    void complete(OrderContext context);

    void cancel(OrderContext context);
}

为了让状态对象能够修改订单并调用外部能力,可以传入一个上下文。上下文不应无限膨胀,否则状态类会变成另一个万能 Service:

public interface OrderContext {

    OrderStatus status();

    void changeTo(OrderStatus targetStatus);

    void recordPayment();

    void arrangeShipment();

    void releaseInventory();

    void refundPayment();
}

实际项目中,OrderContext 可以由订单聚合根实现,也可以是一个更窄的领域服务接口。关键是不要把 Repository、HTTP 客户端、所有系统服务都直接暴露给每个状态类。

第三步:提供默认拒绝行为,避免重复代码

并不是每个状态都允许所有动作。可以在抽象基类中统一处理不允许的操作:

public abstract class AbstractOrderState implements OrderState {

    protected IllegalStateException notAllowed(String action) {
        return new IllegalStateException(
                "订单当前状态不允许执行操作:" + action);
    }

    @Override
    public void pay(OrderContext context) {
        throw notAllowed("支付");
    }

    @Override
    public void ship(OrderContext context) {
        throw notAllowed("发货");
    }

    @Override
    public void complete(OrderContext context) {
        throw notAllowed("完成");
    }

    @Override
    public void cancel(OrderContext context) {
        throw notAllowed("取消");
    }
}

然后分别实现具体状态:

public final class CreatedState extends AbstractOrderState {

    @Override
    public void pay(OrderContext context) {
        context.recordPayment();
        context.changeTo(OrderStatus.PAID);
    }

    @Override
    public void cancel(OrderContext context) {
        context.releaseInventory();
        context.changeTo(OrderStatus.CANCELLED);
    }
}

public final class PaidState extends AbstractOrderState {

    @Override
    public void ship(OrderContext context) {
        context.arrangeShipment();
        context.changeTo(OrderStatus.SHIPPED);
    }

    @Override
    public void cancel(OrderContext context) {
        context.refundPayment();
        context.changeTo(OrderStatus.REFUNDED);
    }
}

public final class ShippedState extends AbstractOrderState {

    @Override
    public void complete(OrderContext context) {
        context.changeTo(OrderStatus.COMPLETED);
    }
}

状态对象通常可以设计为无状态对象,并在工厂中复用。它们不应该保存订单编号、用户信息或本次请求的数据,否则单例复用时会产生线程安全问题。

第四步:让订单聚合根统一管理状态切换

订单对象不应该允许调用者随意 setStatus。如果任何代码都可以直接修改状态,那么状态模式只是表面上的封装,规则仍然可能被绕过。

public class Order implements OrderContext {

    private final Long id;
    private OrderStatus status;
    private final OrderStateFactory stateFactory;

    public Order(Long id, OrderStatus status, OrderStateFactory stateFactory) {
        this.id = id;
        this.status = status;
        this.stateFactory = stateFactory;
    }

    public void pay() {
        currentState().pay(this);
    }

    public void ship() {
        currentState().ship(this);
    }

    public void complete() {
        currentState().complete(this);
    }

    public void cancel() {
        currentState().cancel(this);
    }

    private OrderState currentState() {
        return stateFactory.get(status);
    }

    @Override
    public OrderStatus status() {
        return status;
    }

    @Override
    public void changeTo(OrderStatus targetStatus) {
        this.status = targetStatus;
    }

    @Override
    public void recordPayment() {
        // 记录支付时间、支付流水等领域动作
    }

    @Override
    public void arrangeShipment() {
        // 创建发货任务,实际系统中通常还需要事务或领域事件
    }

    @Override
    public void releaseInventory() {
        // 释放已预占库存
    }

    @Override
    public void refundPayment() {
        // 创建退款请求,不能假设第三方调用一定同步成功
    }
}

状态工厂负责把持久化状态转换为行为对象:

public class OrderStateFactory {

    private final Map<OrderStatus, OrderState> states;

    public OrderStateFactory() {
        Map<OrderStatus, OrderState> map = new EnumMap<>(OrderStatus.class);
        map.put(OrderStatus.CREATED, new CreatedState());
        map.put(OrderStatus.PAID, new PaidState());
        map.put(OrderStatus.SHIPPED, new ShippedState());
        map.put(OrderStatus.COMPLETED, new AbstractOrderState() { });
        map.put(OrderStatus.CANCELLED, new AbstractOrderState() { });
        map.put(OrderStatus.REFUNDED, new AbstractOrderState() { });
        this.states = Collections.unmodifiableMap(map);
    }

    public OrderState get(OrderStatus status) {
        OrderState state = states.get(status);
        if (state == null) {
            throw new IllegalArgumentException("未配置订单状态:" + status);
        }
        return state;
    }
}

示例需要导入 java.util.Collectionsjava.util.EnumMapjava.util.Map。在真实项目里,也可以把终态实现成专门的 FinalState,让“终态不可操作”表达得更清晰。

事务边界不能被设计模式掩盖

状态模式只解决职责分配,不自动解决事务一致性。比如支付动作中,支付记录写入、订单状态更新和库存处理是否在同一个事务内,需要根据业务事实判断。

尤其要注意以下情况:

  • 状态已经改成 PAID,但支付流水还没真正成功。
  • 退款接口调用超时,订单是否应该立即进入 REFUNDED
  • 发货动作由消息异步执行时,订单状态与物流状态是否要拆开。
  • 多个请求同时调用 cancelpay 时,是否有乐观锁或其他并发控制。

比较稳妥的做法是把“业务事实”与“外部动作结果”区分开。例如,退款请求已创建,不一定等于退款成功,可以增加 REFUNDING 状态,或者使用独立的退款单记录过程。不要为了减少枚举值,把多个真实阶段压缩成一个含义模糊的状态。

常见坑:把状态模式改成更复杂的 if-else

第一种坑是状态类里继续判断所有其他状态。比如 PaidState 中又写 if (订单类型...)if (渠道...)if (用户等级...)。这样只是把原来的大分支搬到了多个小文件,复杂度没有消失。

第二种坑是状态类直接依赖大量基础设施。状态类一旦同时调用库存、支付、消息、物流和数据库,就很难单元测试,也很难判断一次动作的事务边界。可以通过领域服务接口、事件发布器或应用层编排来收窄依赖。

第三种坑是让调用方直接操作状态字段。只要仍然存在公开的 setStatus,任何控制器、定时任务或脚本都可能绕过流转规则。状态修改入口应该尽量集中。

第四种坑是为了模式而模式。如果状态只有两个,并且规则稳定、变化很少,简单的枚举加一个清晰的转换表可能更合适。设计模式的价值不在类的数量,而在于它是否降低了变化带来的影响范围。

如何验证重构确实改善了代码

重构后不要只看目录是否“更面向对象”,还要验证行为是否变得可观察、可测试。至少可以覆盖以下测试:

@Test
void createdOrderCanBePaid() {
    Order order = new Order(1L, OrderStatus.CREATED, new OrderStateFactory());

    order.pay();

    assertEquals(OrderStatus.PAID, order.status());
}

@Test
void shippedOrderCannotBeCancelled() {
    Order order = new Order(1L, OrderStatus.SHIPPED, new OrderStateFactory());

    assertThrows(IllegalStateException.class, order::cancel);
}

还应测试副作用是否发生。例如待支付订单取消时释放库存,已支付订单取消时发起退款;不允许的动作则不应调用任何外部服务。若使用 Mock,需要验证调用次数,而不是只验证最终状态。

实践建议

在实际重构中,可以按以下顺序推进:

  1. 先画出当前状态和动作的转换表,找出非法路径与隐藏副作用。
  2. 先把字符串状态替换为枚举,减少拼写错误和散落常量。
  3. 保留原有接口,内部逐步把状态判断迁移到状态对象中。
  4. 每迁移一个动作,就补充成功、失败和非法状态测试。
  5. 最后再处理异步事件、事务拆分和并发控制,不要一次改变所有边界。

如果状态转换已经呈现明显的网状关系,还可以进一步使用显式转换表,把“源状态、动作、目标状态”作为数据管理。但无论采用状态类还是转换表,都应该让非法转换尽早失败,让规则能够被读懂、被测试、被审查。

总结

状态模式并不是消灭所有条件判断,而是把“不同状态下的不同行为”放回状态本身。订单聚合根负责守住入口,状态对象负责表达规则,应用层和基础设施负责事务、持久化以及外部协作。

真正有价值的重构,通常不是把一个大类拆成十个小类,而是重新划清变化边界:新增一种状态时,应该主要影响状态定义和对应实现;修改某个状态的动作规则时,不应迫使调用者理解全部订单流程。

当一段代码的分支已经开始描述一张复杂的业务流程图,与其继续增加注释和嵌套,不如先把状态、动作和副作用分别说清楚。代码一旦能够准确表达业务约束,维护它的人才不必依靠记忆来避免下一次修改出错。

最后修改:2026 年 09 月 04 日
如果觉得我的文章对你有用,请随意赞赏