状态模式(State):让对象的行为随状态而变

状态模式,说白了就是「对象内部状态变了,它的行为也跟着变」。听起来像废话?嗯,你想想看,我们平时写代码,遇到状态判断,第一反应就是 if-else 或者 switch-case。但一旦状态多了,条件分支就会膨胀得像个气球,改一个地方可能炸一片。

我个人习惯用状态模式来替代这种「面条式」的条件判断。它把每个状态封装成一个独立的类,状态之间的切换由状态类自己负责。这样主对象就干净了,只保留一个指向当前状态的引用。

状态模式的结构

状态模式有三个核心角色:

  • Context(上下文):持有当前状态的引用,对外暴露统一的接口。它自己不处理行为,而是委托给状态对象。
  • State(抽象状态):定义所有状态必须实现的接口。通常是一个抽象类或接口。
  • ConcreteState(具体状态):实现状态相关的行为,并且负责在合适的时机切换到下一个状态。

我画了一张结构图,帮你理清它们的关系:

Context - state: State + request() 委托给 state.handle() State(接口) + handle(Context) ConcreteStateA + handle(): 切换到状态B ConcreteStateB + handle(): 切换到状态C ConcreteStateC

你看,Context 只认识 State 接口,具体状态之间怎么跳转,它根本不管。这就是「把变化封装起来」的典型做法。

状态模式 vs 策略模式

这两个模式长得太像了,很多初学者会搞混。我当年也踩过这个坑——有一次把订单状态流转用策略模式实现,结果状态切换逻辑散得到处都是,维护起来想骂人。

它们的核心区别在于:

维度 状态模式 策略模式
目的 管理对象的状态切换 封装可互换的算法
状态/策略间关系 状态之间会相互切换 策略之间彼此独立
谁控制切换 状态类自己决定下一个状态 客户端选择策略
典型场景 订单状态、工作流 排序算法、支付方式

说白了,策略模式是「换算法」,状态模式是「换身份」。你用策略模式时,客户端主动选一个策略塞进去。用状态模式时,对象自己根据当前状态决定下一步去哪。

一句话记住:策略模式是「我选谁」,状态模式是「我是谁」。

状态模式在订单状态流转中的应用

订单系统是状态模式最经典的战场。一个订单从创建到完成,要经历:待支付 → 已支付 → 已发货 → 已收货 → 已完成。每个状态下能做的操作完全不同。

我曾经接手过一个老项目,订单状态判断写在一个大 Service 类里,光 switch-case 就有七八层。加一个新状态要改三个地方,还经常漏掉。后来我用状态模式重构了它。

先看抽象状态接口:

public interface OrderState {
    void pay(OrderContext context);
    void ship(OrderContext context);
    void receive(OrderContext context);
    void cancel(OrderContext context);
}

然后是实现「待支付」状态:

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

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

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

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

再看「已支付」状态:

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

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

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

    @Override
    public void cancel(OrderContext context) {
        System.out.println("已支付订单取消,进入退款流程");
        context.setState(new RefundingState());
    }
}

最后是 Context 类:

public class OrderContext {
    private OrderState state;

    public OrderContext() {
        this.state = new UnpaidState(); // 初始状态
    }

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

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

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

    public void receive() {
        state.receive(this);
    }

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

我的经验:状态模式特别适合状态数量固定、状态转换规则明确的场景。如果状态经常增删,或者转换逻辑频繁变化,那就要考虑是不是该用状态机框架了。

避坑指南

我曾经在一个项目中,把状态对象设计成单例,想着省内存。结果状态之间共享了不该共享的数据,导致并发下状态错乱。后来我改成每次切换都 new 一个新实例,问题就解决了。

还有一点要注意:状态类里不要持有 Context 的强引用,否则容易造成循环依赖。我习惯把 Context 作为参数传进去,而不是存在状态对象的字段里。

警告:状态模式不是万能的。如果状态只有两三个,或者状态转换逻辑极其简单,用 if-else 反而更清晰。别为了用模式而用模式。

总结

状态模式的核心思想就一句话:把状态相关的行为封装到独立的状态类中,让对象的状态切换变得透明且可维护。它和策略模式的区别在于「切换由谁控制」——状态模式是状态自己决定下一步,策略模式是客户端决定用哪个算法。

订单状态流转是状态模式的经典应用。每个状态只关心自己能做什么、做完后去哪。这样加一个新状态,只需要新增一个类,改一下相关状态的转换逻辑,完全符合开闭原则。

嗯,如果你现在正被一堆 if-else 折磨,不妨试试状态模式。但记住,别过度设计。


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