状态模式:状态转换、消除状态判断、实战:订单状态机

状态模式,说白了就是让对象在内部状态改变时,自动切换行为。听起来有点抽象?我举个例子你就明白了。

你想想看,一个订单系统里,订单的状态有:待支付、已支付、已发货、已完成、已取消。每个状态下,用户能做的操作完全不同。比如待支付状态下可以取消,已支付状态下就不能取消了。传统写法是什么?一堆 if-else 或者 switch-case,对吧?

这种写法的问题很明显:每增加一个状态,就要改一大片代码。我在项目中遇到过这种痛,改一个状态逻辑,结果影响了三个地方,测试回归了整整两天。嗯,从那以后,我对状态模式就特别上心。

状态模式的核心思想

状态模式的核心,就是把每个状态封装成一个独立的类。每个类只负责自己状态下的行为逻辑。这样,状态转换就变成了对象替换——说白了,就是换一个状态对象。

我个人的习惯是,先画一张状态转换图,把所有的状态和允许的转换路径理清楚。这一步做不好,后面写代码就是给自己挖坑。

状态模式三要素:

  • 上下文(Context):持有当前状态对象,对外暴露行为接口
  • 抽象状态(State):定义所有状态共有的行为接口
  • 具体状态(ConcreteState):实现特定状态下的行为逻辑
Context(上下文) State(抽象状态) 待支付状态 已支付状态 已发货状态 支付 发货 状态模式结构:上下文持有状态对象,状态转换通过替换状态对象实现

消除状态判断:从 if-else 到多态

传统写法里,状态判断长这样:

// 传统写法:满屏的 if-else
public void cancelOrder(Order order) {
    if (order.getStatus() == OrderStatus.PENDING_PAYMENT) {
        // 待支付可以取消
        order.setStatus(OrderStatus.CANCELLED);
    } else if (order.getStatus() == OrderStatus.PAID) {
        // 已支付需要退款
        refund(order);
        order.setStatus(OrderStatus.CANCELLED);
    } else if (order.getStatus() == OrderStatus.SHIPPED) {
        // 已发货不能取消
        throw new IllegalStateException("已发货订单不能取消");
    }
    // ... 继续 else if
}

这种代码,每加一个状态就要加一个 else if。而且多个操作(取消、支付、发货)都要写类似的判断逻辑。维护起来,真的会让人崩溃。

用状态模式改写之后:

// 抽象状态接口
public interface OrderState {
    void pay(OrderContext context);
    void cancel(OrderContext context);
    void ship(OrderContext context);
    void complete(OrderContext context);
}

// 待支付状态
public class PendingPaymentState implements OrderState {
    @Override
    public void pay(OrderContext context) {
        System.out.println("支付成功,进入已支付状态");
        context.setState(new PaidState());
    }

    @Override
    public void cancel(OrderContext context) {
        System.out.println("取消订单,进入已取消状态");
        context.setState(new CancelledState());
    }

    @Override
    public void ship(OrderContext context) {
        throw new IllegalStateException("未支付不能发货");
    }

    @Override
    public void complete(OrderContext context) {
        throw new IllegalStateException("未支付不能完成");
    }
}

// 已支付状态
public class PaidState implements OrderState {
    @Override
    public void pay(OrderContext context) {
        throw new IllegalStateException("已支付,不能重复支付");
    }

    @Override
    public void cancel(OrderContext context) {
        System.out.println("退款处理中...");
        context.setState(new CancelledState());
    }

    @Override
    public void ship(OrderContext context) {
        System.out.println("发货成功,进入已发货状态");
        context.setState(new ShippedState());
    }

    @Override
    public void complete(OrderContext context) {
        throw new IllegalStateException("未发货不能完成");
    }
}

我的经验:状态模式最爽的地方在于,每个状态类只关心自己允许的操作。不允许的操作直接抛异常,逻辑清晰到不行。我曾经在一个电商项目中用这种方式重构了订单模块,代码量减少了40%,bug率直线下降。

实战:订单状态机

好了,理论说完了,咱们直接上一个完整的订单状态机。这个例子我用了很多次,每次讲状态模式都拿它当案例。

先定义状态转换表:

当前状态 支付 取消 发货 完成
待支付 → 已支付 → 已取消
已支付 → 已取消(退款) → 已发货
已发货 → 已完成
已完成
已取消

接下来是上下文类:

public class OrderContext {
    private OrderState currentState;

    public OrderContext() {
        // 初始状态:待支付
        this.currentState = new PendingPaymentState();
    }

    public void setState(OrderState state) {
        this.currentState = state;
    }

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

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

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

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

使用起来非常直观:

public class Demo {
    public static void main(String[] args) {
        OrderContext order = new OrderContext();

        // 待支付 -> 支付 -> 已支付
        order.pay();

        // 已支付 -> 发货 -> 已发货
        order.ship();

        // 已发货 -> 完成 -> 已完成
        order.complete();

        // 已完成状态下尝试取消,会抛异常
        // order.cancel(); // IllegalStateException
    }
}

避坑指南:我曾经犯过一个错误——在状态类里直接操作数据库。记住,状态类只负责状态转换逻辑,不要在里面写持久化代码。正确的做法是:状态类返回转换结果,由上下文或外部服务处理持久化。否则,你的状态类会变得又臭又硬,难以测试。

状态模式的优缺点

说实话,状态模式也不是万能的。我总结了一下它的优缺点:

  • 优点:
    • 消除了大量的条件判断语句
    • 每个状态独立,符合单一职责原则
    • 新增状态非常方便,开闭原则的完美体现
    • 状态转换逻辑集中,容易维护
  • 缺点:
    • 状态类数量会增多,如果状态很少,反而过度设计
    • 状态类之间可能有依赖,比如取消状态需要知道退款逻辑
    • 状态转换的复杂度如果很高,状态类之间的交互会变得复杂

我的建议:如果你的状态数量超过3个,而且每个状态下的行为差异很大,那就果断用状态模式。如果只有2个状态,或者状态之间行为差异很小,用简单的 if-else 反而更清晰。别为了用模式而用模式,这是很多新手容易犯的毛病。

最后说一句,状态模式在实际项目中用得非常多。除了订单系统,工作流引擎、游戏角色状态、网络连接状态,都是它的典型应用场景。你想想看,这些场景是不是都有共同点?状态多、转换规则复杂、每个状态行为不同。嗯,这就是状态模式的用武之地。

公众号:蓝海资料掘金营,微信deep3321