状态模式(State):订单状态机

状态模式,说白了就是「一个对象内部状态变了,它的行为也跟着变」。听起来有点绕?我举个例子你就明白了——你想想看,电商订单在不同状态下,能做的事情完全不一样。

待支付的订单可以「取消」,但已发货的订单就不能取消了。已收货的订单可以「申请售后」,但待支付的就不行。每个状态都有自己的一套行为规则。这就是状态模式要解决的问题。

为什么需要状态模式?

我在项目中遇到过这样的场景:早期写的订单处理代码,全是 if-else 判断状态。一个方法里十几个 if,看得人头皮发麻。后来订单状态从 4 个增加到 7 个,代码直接炸了——改一个状态,影响七八个方法。

状态模式的核心思想很简单:把每个状态封装成独立的类。每个类只关心自己状态下的行为逻辑。这样新增状态时,不会影响其他状态的代码。

状态模式三要素:

  • 上下文(Context):持有当前状态对象,对外暴露统一接口
  • 抽象状态(State):定义所有状态的公共接口
  • 具体状态(ConcreteState):实现每个状态下的具体行为

电商订单状态机实战

我们先看一个典型的电商订单状态流转图:

待支付 已支付 已发货 已收货 支付 发货 收货 取消订单 已完成 确认完成

嗯,这里要注意:状态流转不是乱转的。每个状态只允许特定的「事件」触发状态变更。比如「待支付」只能通过「支付事件」变成「已支付」,不能直接跳到「已发货」。

代码实现

我们先定义订单状态的抽象接口:

// 抽象状态接口
public interface OrderState {
    // 支付
    void pay(OrderContext context);
    // 发货
    void ship(OrderContext context);
    // 收货
    void receive(OrderContext context);
    // 取消
    void cancel(OrderContext context);
    // 获取当前状态描述
    String getStateName();
}

然后实现具体的状态类。每个类只关心自己状态下的行为:

// 待支付状态
public class PendingPaymentState implements OrderState {
    @Override
    public void pay(OrderContext context) {
        System.out.println("支付成功,订单变为已支付状态");
        context.setState(new PaidState());
    }
    
    @Override
    public void ship(OrderContext context) {
        throw new RuntimeException("待支付订单不能发货");
    }
    
    @Override
    public void receive(OrderContext context) {
        throw new RuntimeException("待支付订单不能收货");
    }
    
    @Override
    public void cancel(OrderContext context) {
        System.out.println("订单已取消");
        context.setState(new CancelledState());
    }
    
    @Override
    public String getStateName() {
        return "待支付";
    }
}

// 已支付状态
public class PaidState implements OrderState {
    @Override
    public void pay(OrderContext context) {
        throw new RuntimeException("订单已支付,不能重复支付");
    }
    
    @Override
    public void ship(OrderContext context) {
        System.out.println("发货成功,订单变为已发货状态");
        context.setState(new ShippedState());
    }
    
    @Override
    public void receive(OrderContext context) {
        throw new RuntimeException("未发货不能收货");
    }
    
    @Override
    public void cancel(OrderContext context) {
        System.out.println("订单已取消,退款处理中");
        context.setState(new CancelledState());
    }
    
    @Override
    public String getStateName() {
        return "已支付";
    }
}

上下文类持有当前状态,所有操作委托给状态对象:

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 ship() {
        currentState.ship(this);
    }
    
    public void receive() {
        currentState.receive(this);
    }
    
    public void cancel() {
        currentState.cancel(this);
    }
    
    public String getCurrentState() {
        return currentState.getStateName();
    }
}

TCP 连接状态机

另一个经典场景是 TCP 连接状态。我记得当年学网络协议时,TCP 的状态图看得我头晕。用状态模式来实现,思路就清晰多了。

当前状态 事件 下一状态 行为
CLOSED connect() LISTEN 发起连接请求
LISTEN accept() ESTABLISHED 接受连接
ESTABLISHED close() CLOSED 关闭连接
ESTABLISHED send() ESTABLISHED 发送数据

代码结构和订单状态机类似,只是状态名称和业务逻辑不同。这里就不重复贴代码了。

我的经验:状态模式特别适合那些「状态多、状态间转换规则复杂」的场景。但如果你只有两三个状态,用 if-else 反而更简单。别为了用设计模式而用设计模式。

状态模式 vs 策略模式

很多人容易把状态模式和策略模式搞混。我简单说下区别:

  • 策略模式:客户端主动选择策略,策略之间是平级的、可互换的
  • 状态模式:状态由内部自动流转,客户端不知道状态变化

说白了,策略模式是「我选你」,状态模式是「它自己变」。

避坑指南:我曾经在一个项目中,把状态转换逻辑写在了状态类里。结果状态类之间互相依赖,形成了循环引用。后来我把状态转换规则抽到了上下文类中,用一张转换表来管理,问题就解决了。

状态模式的优缺点

优点:

  • 消除了庞大的条件分支语句
  • 每个状态独立变化,符合开闭原则
  • 状态流转逻辑清晰,易于维护

缺点:

  • 状态类数量会增多
  • 如果状态转换规则频繁变化,维护成本也不低

嗯,总的来说,状态模式是处理「状态驱动行为」问题的利器。你在实际项目中遇到类似场景,不妨试试这个模式。代码结构清晰了,后面的人维护起来也轻松很多。

核心要点回顾:

  1. 状态模式把每个状态封装成独立类
  2. 上下文委托给当前状态对象处理请求
  3. 状态流转由内部自动完成,客户端无感知
  4. 适合状态多、转换规则复杂的场景

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