跳转至

状态模式

从糖果机说起

你有一台糖果机,它有 4 种状态:

  • 售罄(SoldOut
  • 没有投币(NoQuarter
  • 已投币(HasQuarter
  • 正在出售(Sold

每种操作(投币/退币/转把手/发糖果)在不同状态下行为完全不同。最直觉的实现是用一个整型常量代表状态,每个操作都写一堆 if-else——这在书里被叫做"条件状态机"。

问题来了:产品经理说要加一个**赢奖状态**(10% 几率发两颗糖果)。你必须在每一个方法里都加 if (state == WINNER) 分支——改动散落各处,极易出错。

解决方案:把每种状态封装成独立的类,让状态自己处理所有动作,并决定下一个状态是什么。GumballMachine 本身变成了一个纯粹的委托者,没有任何条件分支。

🔍 定义

状态模式(State)允许对象在内部状态发生改变时改变其行为,看起来像是改变了对象的类。将每种状态封装为独立的类,消除大量的条件分支。

⚠️ 不使用状态模式存在的问题

StateBadExample.java
package com.example.behavioral.state;

/**
 * 状态模式 - 反例
 * 问题:订单状态逻辑全部堆在 OrderBad 内,每次新增状态都要修改整个类
 */
public class StateBadExample {
    public static void main(String[] args) {
        OrderBad order = new OrderBad();
        order.pay();     // PENDING -> PAID
        order.ship();    // PAID -> SHIPPED
        order.complete();// SHIPPED -> COMPLETED
        order.pay();     // ❌ 已完成订单不能再支付
    }
}

// ❌ 所有状态迁移逻辑都堆在一个类里
class OrderBad {
    private String status = "PENDING"; // 用字符串表示状态

    public void pay() {
        if ("PENDING".equals(status)) {
            System.out.println("支付成功,状态: PENDING -> PAID");
            status = "PAID";
        } else {
            System.out.println("❌ 当前状态[" + status + "]不能支付");
        }
    }

    public void ship() {
        if ("PAID".equals(status)) {
            System.out.println("已发货,状态: PAID -> SHIPPED");
            status = "SHIPPED";
        } else {
            System.out.println("❌ 当前状态[" + status + "]不能发货");
        }
    }

    public void complete() {
        if ("SHIPPED".equals(status)) {
            System.out.println("订单完成,状态: SHIPPED -> COMPLETED");
            status = "COMPLETED";
        } else {
            System.out.println("❌ 当前状态[" + status + "]不能完成");
        }
    }
    // 新增"退款"状态?要在每个方法里加 if-else ❌
}

🏗️ 设计模式结构(糖果机)

stateDiagram-v2
    [*] --> NoQuarter : 初始化(有糖果)
    NoQuarter --> HasQuarter : insertQuarter()
    HasQuarter --> NoQuarter : ejectQuarter()
    HasQuarter --> Sold : turnCrank()
    Sold --> NoQuarter : dispense()(还有糖果)
    Sold --> SoldOut : dispense()(已售罄)
    SoldOut --> [*]
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#768390'}}}%%
classDiagram
    classDef default fill:transparent,stroke:#768390
    class GumballState {
        <<interface>>
        +insertQuarter() void
        +ejectQuarter() void
        +turnCrank() void
        +dispense() void
    }
    class NoQuarterState
    class HasQuarterState
    class SoldState
    class SoldOutState
    class GumballMachine {
        -state: GumballState
        -count: int
        +insertQuarter() void
        +ejectQuarter() void
        +turnCrank() void
        +setState(s) void
    }
    GumballState <|.. NoQuarterState
    GumballState <|.. HasQuarterState
    GumballState <|.. SoldState
    GumballState <|.. SoldOutState
    GumballMachine o--> GumballState
    note for GumballState "状态接口(State)"
    note for NoQuarterState "具体状态(ConcreteState)"
    note for GumballMachine "上下文(Context)"
角色 说明
GumballState(状态接口) 定义所有可能的动作
NoQuarterState 等(具体状态) 实现该状态下的行为,并负责转换到下一状态
GumballMachine(上下文) 持有当前状态,委托所有动作;无任何条件分支

💻 设计模式举例说明

StateExample.java
package com.example.behavioral.state;

/**
 * 状态模式 - 正例
 * 每个状态独立封装,新增状态只需添加新类,不修改 Order
 */
public class StateExample {
    public static void main(String[] args) {
        Order order = new Order();
        order.pay();      // PENDING -> PAID   ✅
        order.ship();     // PAID -> SHIPPED   ✅
        order.complete(); // SHIPPED -> COMPLETED ✅
        order.pay();      // ❌ 已完成,拒绝操作
    }
}

// 状态接口
interface OrderState {
    void pay(Order order);
    void ship(Order order);
    void complete(Order order);
}

// 状态:待支付
class PendingState implements OrderState {
    @Override
    public void pay(Order order) {
        System.out.println("✅ 支付成功 PENDING -> PAID");
        order.setState(new PaidState());
    }
    @Override public void ship(Order order)    { System.out.println("❌ 待支付订单不能发货");   }
    @Override public void complete(Order order){ System.out.println("❌ 待支付订单不能完成");   }
}

// 状态:已支付
class PaidState implements OrderState {
    @Override public void pay(Order order)     { System.out.println("❌ 已支付,不能重复支付"); }
    @Override
    public void ship(Order order) {
        System.out.println("✅ 发货成功 PAID -> SHIPPED");
        order.setState(new ShippedState());
    }
    @Override public void complete(Order order){ System.out.println("❌ 已支付订单须先发货");   }
}

// 状态:已发货
class ShippedState implements OrderState {
    @Override public void pay(Order order)     { System.out.println("❌ 已发货,不能支付");     }
    @Override public void ship(Order order)    { System.out.println("❌ 已发货,不能重复发货"); }
    @Override
    public void complete(Order order) {
        System.out.println("✅ 订单完成 SHIPPED -> COMPLETED");
        order.setState(new CompletedState());
    }
}

// 状态:已完成(终态)
class CompletedState implements OrderState {
    @Override public void pay(Order order)     { System.out.println("❌ 已完成订单不能支付");   }
    @Override public void ship(Order order)    { System.out.println("❌ 已完成订单不能发货");   }
    @Override public void complete(Order order){ System.out.println("❌ 已完成订单不能再完成"); }
}

// 上下文:Order 持有当前状态,所有操作委托给状态对象
class Order {
    private OrderState state = new PendingState(); // 初始状态:待支付

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

    // 委托给当前状态处理
    public void pay()      { state.pay(this);      }
    public void ship()     { state.ship(this);     }
    public void complete() { state.complete(this); }
}

新增赢奖状态(WinnerState)有多简单

只需新建 WinnerState implements GumballState,在 HasQuarterState.turnCrank() 中用随机数决定切换到 SoldState 还是 WinnerStateGumballMachine 和其他所有状态类**一行都不用改**——这正是状态模式的威力。

⚖️ 优缺点

优点:

  • 消除大量条件分支,每种状态的行为集中在一个类中
  • 符合**开闭原则**:新增状态只需新增类,不修改已有状态类
  • 状态转换逻辑显式且集中(在状态类内部)

缺点:

  • 每种状态一个类,类数量增多
  • 状态和转换关系很简单时,可能过度设计

🔗 与其它模式的关系

模式 切换时机 切换依据 谁决定切换
状态(State) 内部状态变化时 对象自身状态 状态类自己
策略(Strategy) 客户端主动注入 外部运行时选择 客户端

两者结构相似(都持有一个"行为对象"),核心区别:状态模式的切换由**状态类自己触发**;策略模式的切换由**客户端主动选择**。

🗂️ 应用场景

  • 对象行为随内部状态大幅变化(TCP 连接状态机、工作流审批、游戏角色、电商订单)
  • 需要消除大量与状态相关的条件分支
  • Spring Statemachine 就是状态模式的企业级实现

🏭 工业视角

状态机的三种实现方式

状态机(Finite State Machine, FSM)有三种经典实现方式,选哪种取决于**状态/事件数量**与**每种状态下动作的复杂程度**:

实现方式 适用场景 主要缺点
分支逻辑(if-else/switch) 状态少,逻辑简单 状态一多,分支爆炸,极难维护
查表法(二维状态转移矩阵) 状态多,动作逻辑简单(如加减分) 动作复杂时(写 DB、发消息)无法用数组表示
状态模式(State Pattern) 状态少,但每种状态的动作复杂 类数量增多

查表法的精髓是把"状态转移图"直接编码为二维矩阵,修改规则只改数组,甚至可以从配置文件加载,实现**零代码变更**的规则调整:

查表法:状态转移矩阵(马里奥示例)
// 行 = 当前状态,列 = 触发事件
private static final State[][] transitionTable = {
//  GOT_MUSHROOM  GOT_CAPE  GOT_FIRE  MET_MONSTER
    {SUPER,       CAPE,     FIRE,     SMALL},  // SMALL
    {SUPER,       CAPE,     FIRE,     SMALL},  // SUPER
    {CAPE,        CAPE,     CAPE,     SMALL},  // CAPE
    {FIRE,        FIRE,     FIRE,     SMALL},  // FIRE
};

private void executeEvent(Event event) {
    int s = currentState.getValue(), e = event.getValue();
    this.currentState = transitionTable[s][e];
    this.score       += actionTable[s][e];
}

状态模式 vs 策略模式:结构相似,意图不同

两者都将行为委托给一个独立的"策略/状态"对象,容易混淆,但有本质区别:

  • 状态模式:状态类之间会互相迁移(SmallMario.obtainMushRoom() 内部将 Context 切换到 SuperMario),切换是**内部自驱**的,Context 感知不到。
  • 策略模式:策略之间没有关联,也不知道彼此存在;切换由**外部客户端主动注入**,Context 被动接收。

实战选型建议

订单状态机(待支付 → 已支付 → 待发货 → 已完成 → 已取消):如果每个状态转移只是简单的字段更新,用查表法;如果每次转移需要发短信、写 MQ、扣库存等复杂动作,用状态模式。Spring Statemachine 是状态模式的企业级落地,内置持久化、异步动作、Guard 条件守卫等能力。

状态模式中的双向依赖

状态类(如 SmallMario)需要回调 Context(MarioStateMachine)来更新积分和当前状态,会产生双向依赖。可将状态类设计为**单例**,通过方法参数传入 Context,而非构造函数注入,以减轻循环依赖的耦合程度。