什么是状态模式?应用场景有哪些?


面试考察点

  1. 痛点识别能力:面试官想知道你是否遇到过 "状态多了,if-else 爆炸" 的场景,以及你是否能意识到这是一个设计问题而非业务问题。

  2. 模式理解深度:能否讲清楚 "把状态的行为封装成独立类,通过组合替代判断" 这个核心思路,而不是只会背一句 "对象行为随状态改变而改变"。

  3. 实战应用:能否举出订单状态机、工作流引擎等实际应用场景,证明你真的在项目中用过或者见过这个模式。

核心答案

一句话定义:状态模式允许一个对象在其内部状态改变时改变它的行为,对象看起来好像修改了它的类。

说人话就是:当一个对象有多种状态,不同状态下对同一操作的行为不同时,不要用 if-else 硬编码,而是把每个状态的行为封装成独立的类,对象持有当前状态的引用,操作委托给状态对象来执行。

生活中的例子:自动售货机。投币前你按 "出货" 没反应,投币后按 "出货" 吐商品,商品出完按 "出货" 显示 "已售罄"。同一个按钮,三种状态三种行为。你当然可以用 if (state == XX) 来写,但状态多了以后代码就炸了。

上面的图展示了状态模式的核心结构:

  • Context(上下文):持有当前状态的引用,对外提供操作方法。客户端只跟 Context 打交道,不知道当前具体是哪个状态。
  • State(状态接口):定义所有状态下共有的操作方法。
  • ConcreteState(具体状态):每个状态类实现自己的行为逻辑,并决定状态转换规则。

关键点在于:状态的切换逻辑分散在各个状态类内部,而不是集中在 Context 的一个大 switch-case。新增状态只需要新增一个类,完全不影响已有代码。

深度解析

一、不用状态模式:if-else 地狱

先看看不用状态模式的代码长什么样,你就知道痛点在哪了。

// 订单状态常量
public static final int UNPAID = 0;      // 待付款
public static final int PAID = 1;        // 已付款
public static final int SHIPPED = 2;     // 已发货
public static final int COMPLETED = 3;   // 已完成

public class Order {

    private int state;

    // 付款操作
    public void pay() {
        if (state == UNPAID) {
            System.out.println("付款成功");
            state = PAID;
        } else if (state == PAID) {
            System.out.println("已付款,请勿重复操作");
        } else if (state == SHIPPED) {
            System.out.println("已发货,无法操作");
        } else {
            System.out.println("订单已完成");
        }
    }

    // 发货操作
    public void ship() {
        if (state == UNPAID) {
            System.out.println("请先付款");
        } else if (state == PAID) {
            System.out.println("发货成功");
            state = SHIPPED;
        } else if (state == SHIPPED) {
            System.out.println("已发货,请勿重复操作");
        } else {
            System.out.println("订单已完成");
        }
    }

    // 确认收货
    public void confirm() {
        if (state == UNPAID) {
            System.out.println("请先付款");
        } else if (state == PAID) {
            System.out.println("请等待发货");
        } else if (state == SHIPPED) {
            System.out.println("确认收货成功");
            state = COMPLETED;
        } else {
            System.out.println("已完成,请勿重复操作");
        }
    }
}

看到了吗?每个操作方法里都有 4 层 if-else。这才 4 个状态、3 个操作。如果状态增加到 8 个,操作增加到 10 个呢?那就是 80 个 if-else 分支。而且新增一个状态,所有方法都得改,违反了开闭原则。

二、用状态模式重构

把每个状态的行为抽成独立类,问题就迎刃而解了。

// 状态接口
public interface OrderState {
    void pay(OrderContext context);
    void ship(OrderContext context);
    void confirm(OrderContext 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 confirm() {
        state.confirm(this);
    }
}

四个具体状态类:

// 待付款状态
public class UnpaidState implements OrderState {
    @Override
    public void pay(OrderContext ctx) {
        System.out.println("付款成功 ✓");
        ctx.setState(new PaidState()); // 切换到已付款
    }

    @Override
    public void ship(OrderContext ctx) {
        System.out.println("请先付款");
    }

    @Override
    public void confirm(OrderContext ctx) {
        System.out.println("请先付款");
    }
}

// 已付款状态
public class PaidState implements OrderState {
    @Override
    public void pay(OrderContext ctx) {
        System.out.println("已付款,请勿重复操作");
    }

    @Override
    public void ship(OrderContext ctx) {
        System.out.println("发货成功 ✓");
        ctx.setState(new ShippedState()); // 切换到已发货
    }

    @Override
    public void confirm(OrderContext ctx) {
        System.out.println("请等待发货");
    }
}

// 已发货状态
public class ShippedState implements OrderState {
    @Override
    public void pay(OrderContext ctx) {
        System.out.println("已付款,无需操作");
    }

    @Override
    public void ship(OrderContext ctx) {
        System.out.println("已发货,请勿重复操作");
    }

    @Override
    public void confirm(OrderContext ctx) {
        System.out.println("确认收货 ✓");
        ctx.setState(new CompletedState()); // 切换到已完成
    }
}

// 已完成状态
public class CompletedState implements OrderState {
    @Override
    public void pay(OrderContext ctx) {
        System.out.println("订单已完成");
    }

    @Override
    public void ship(OrderContext ctx) {
        System.out.println("订单已完成");
    }

    @Override
    public void confirm(OrderContext ctx) {
        System.out.println("已完成,请勿重复操作");
    }
}

使用:

OrderContext order = new OrderContext();

order.ship();     // 请先付款
order.pay();      // 付款成功 ✓
order.pay();      // 已付款,请勿重复操作
order.ship();     // 发货成功 ✓
order.confirm();  // 确认收货 ✓
order.confirm();  // 已完成,请勿重复操作

重构后的代码,每个状态类只关心自己的逻辑,新增状态只需要新增一个类,完全不用动其他状态类的代码Context 的代码也变得极其简洁——每个操作就一行,委托给 state 就完事了。

三、状态模式 vs 策略模式

这两个模式在代码结构上几乎一模一样,面试时特别容易被追问。区别在哪?

对比项状态模式策略模式
核心目的对象行为随内部状态变化算法自由切换,客户端选策略
谁负责切换状态对象自己切换(自动)客户端手动设置策略(被动)
状态/策略是否互相知道状态之间有转换关系,彼此知道策略之间完全独立,互不知道
典型场景订单状态机、工作流支付方式选择、排序算法切换

一句话区分:状态模式的切换是自动的(状态对象内部决定),策略模式的切换是手动的(客户端决定)

四、实际应用场景

1. 电商订单状态流转

最经典的场景。待付款 → 已付款 → 已发货 → 已完成,还有取消、退款等分支状态。用状态模式管理状态转换,逻辑清晰,扩展方便。

2. 工作流引擎

Activiti、Flowable 这些工作流引擎里,流程节点的流转本质上就是状态模式。审批通过从 "待审批" 到 "已审批",驳回到 "待修改",每个节点的操作行为都不同。

3. TCP 连接状态

TCP 协议有 LISTEN、SYN_SENT、ESTABLISHED、CLOSE_WAIT 等多种状态,不同状态下对同一个报文的处理方式完全不同。经典的 TCP 状态机就是状态模式的应用。

4. 游戏角色状态

游戏角色有站立、行走、奔跑、跳跃、受伤等状态,每个状态下按同一个键的行为完全不同。比如跳跃状态下按跳跃是二段跳,站立状态下按跳跃是起跳。

5. Spring StateMachine

Spring 官方提供了 spring-statemachine 框架,专门用来管理复杂的状态流转,底层就是状态模式 + 事件驱动的思想。

面试高频追问

  1. 状态模式和策略模式有什么区别?

    • 结构相似,意图不同。状态模式的切换由状态对象内部自动完成,策略模式由客户端手动设置。状态之间有转换关系,策略之间完全独立。
  2. 什么情况下应该用状态模式?

    • 当一个对象有多个状态,不同状态下对同一操作的行为不同,且状态转换规则复杂时。简单的 2~3 个状态判断用 if-else 就够了,别过度设计。
  3. 状态模式会不会导致类爆炸?

    • 会,每个状态一个类。但相比一个几千行的 switch-case,分散到多个小类里可维护性好得多。这是 "用类的数量换代码清晰度" 的典型权衡。

常见面试变体

  • "如何设计一个订单状态机?"
  • "状态模式和策略模式有什么区别?"
  • "如果不用状态模式,你会怎么处理多状态逻辑?"
  • "你在项目中用过状态模式吗?举一个例子"

记忆口诀

核心思想:状态封装成类,行为跟着状态走,切换由状态自己定。

vs 策略模式:状态自动切换,策略手动选择。

适用时机:状态多、转换复杂、if-else 成灾。

总结

状态模式的精髓就是 "把每个状态的行为封装成独立类,用组合替代 if-else"。面试时先说概念,然后用订单状态机这个例子对比 "重构前" 和 "重构后" 的代码,最后和策略模式做区分。能把 if-else 地狱的痛点讲清楚,再展示重构后的优雅,面试官就知道你不只是背了概念。