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

#中间件#面试八股

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

结论先行:状态模式(State)把"对象在某个状态下的行为"抽成独立的状态类,对象内部持有当前状态对象,行为随之委托给它;状态之间的迁移由状态自己或上下文驱动。它治的是同一个病:一个 switch(state) 散落在七八个方法里,每加一个状态就要改所有分支。

1. 结构:行为跟着状态走

                 ┌──────────────────────────┐
                 │       OrderContext       │
                 │ - state: OrderState      │
                 ├──────────────────────────┤
                 │ + pay()   → state.pay()  │
                 │ + ship()  → state.ship() │
                 │ + setState(s)            │
                 └────────────┬─────────────┘
                              │ 委托
        ┌─────────────────────┼─────────────────────┐
        ▼                     ▼                     ▼
┌────────────────┐   ┌────────────────┐   ┌────────────────┐
│ UnpaidState    │   │  PaidState     │   │ ShippedState   │
│ pay()→Paid     │   │ ship()→Shipped │   │ ship()→拒绝     │
│ ship()→拒绝     │   │ cancel()→已付   │   │ finish()→完成   │
└────────────────┘   └────────────────┘   └────────────────┘

调用方始终只看到 OrderContext.pay();状态不同,同一个方法的表现不同。

2. 状态机视角

      ┌──────────┐  pay   ┌──────────┐  ship  ┌───────────┐  confirm  ┌──────────┐
      │ 待支付    │───────►│ 已支付    │───────►│  已发货    │──────────►│  已完成   │
      └────┬─────┘        └────┬─────┘        └───────────┘           └──────────┘
           │ cancel            │ refund
           ▼                   ▼
      ┌──────────┐        ┌──────────┐
      │  已取消   │        │  退款中   │
      └──────────┘        └──────────┘

把这张图画出来,就能一眼看出:哪些迁移合法、哪些状态是终态、有没有"回退边"。状态模式就是把这幅图写成代码。

3. 代码:订单状态

interface OrderState {
    void pay(OrderContext ctx);
    void ship(OrderContext ctx);
    String name();
}

class UnpaidState implements OrderState {
    public void pay(OrderContext ctx) {
        System.out.println("支付成功");
        ctx.setState(new PaidState());          // 状态自迁移
    }
    public void ship(OrderContext ctx) {
        throw new IllegalStateException("未支付不能发货");   // 非法迁移显式失败
    }
    public String name() { return "待支付"; }
}

class PaidState implements OrderState {
    public void pay(OrderContext ctx) {
        System.out.println("已支付,忽略重复支付");
    }
    public void ship(OrderContext ctx) {
        System.out.println("已发货");
        ctx.setState(new ShippedState());
    }
    public String name() { return "已支付"; }
}

class ShippedState implements OrderState {
    public void pay(OrderContext ctx)  { throw new IllegalStateException("已发货,不能支付"); }
    public void ship(OrderContext ctx) { System.out.println("已发货,忽略重复操作"); }
    public String name() { return "已发货"; }
}

class OrderContext {
    private OrderState state = new UnpaidState();     // 初始状态

    void setState(OrderState state) { this.state = state; }
    String stateName() { return state.name(); }

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

// 使用
OrderContext order = new OrderContext();
order.ship();          // 抛 IllegalStateException:未支付不能发货
order.pay();           // 支付成功 → 迁移到 PaidState
order.ship();          // 已发货 → 迁移到 ShippedState

注意 OrderState 的方法都接收 ctx,这样状态类才能把上下文推到下一个状态——这是状态模式区别于普通策略的关键细节。

4. 状态模式与策略模式

对比项状态模式策略模式
谁决定用哪个实现内部状态自己迁移外部调用方显式指定
实现之间是否互相知道知道(要能跳到下一个状态)通常互不知道
关注点对象生命周期上的阶段同一件事的不同算法
调用方视角只调一次接口,行为随状态变化先选策略,再调用
典型例子订单、审批、TCP 连接计价规则、压缩算法、路由策略

一句话区分:策略是"你选我",状态是"我自己走"。面试常被用来验证你是否真的理解两者的意图差异。

5. 应用场景

场景状态举例
电商订单待支付、已支付、已发货、已完成、已取消、退款中
审批流草稿、待审批、审批中、通过、驳回
工作流引擎Activiti/Flowable 的节点与连线本质上就是状态机
网络连接TCP 的 LISTEN、SYN_SENT、ESTABLISHED、CLOSE_WAIT
游戏角色的待机、移动、攻击、死亡
账号风控正常、限流、冻结、申诉中

6. 常见追问

Q:状态多了会类爆炸吗? 会的。5 个状态 × 4 个动作就是 20 个方法待实现。缓解办法:把"某状态下不支持的动作"收敛到一个抽象基类的默认抛异常实现,子类只覆写合法的迁移;状态超过十几个时,改用表驱动(状态迁移表 + 配置),或引入成熟状态机框架。

Q:状态对象要不要每次 new? 如果状态类无字段(多数情况),可以做成单例常量复用,避免频繁创建;如果要携带进入该状态的时间戳等数据,就必须 new。

Q:状态怎么持久化、怎么恢复? 数据库存状态枚举值而不是对象引用,加载时按枚举构造对应状态类。千万别把状态对象序列化进 DB——重构一次类名就全废了。

Q:并发下会不会两个请求同时迁移? 会。状态迁移必须加乐观锁:update orders set state='PAID' where id=? and state='UNPAID',影响行数为 0 说明状态已被别人改过,直接返回冲突。

小结

  • 状态模式 = 把 switch(state) 拆成状态类,行为委托给当前状态,迁移由状态驱动。
  • 与策略的分水岭:谁来决定用哪个实现——内部状态 vs 外部调用方。
  • 落地要点:非法迁移显式抛错、无状态类可复用、库里存枚举值、迁移加乐观锁;状态再多就转向迁移表。
笔记加载中…