行为型模式-状态模式

概述

状态模式(State Pattern)是 GoF 23 种设计模式中行为型模式的重要一员,其核心意图被经典定义为:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。这句话揭示了状态模式最本质的威力------将原本由大量条件分支控制的行为逻辑,解耦为一个个独立的状态类,使得状态转换逻辑从隐式变为显式,从分散走向集中。

在传统面向过程或简单面向对象的编码中,我们常常使用 if-elseswitch-case 依据某个状态变量来执行不同的业务逻辑。随着状态的增多和状态转换规则的复杂化,这些条件分支会迅速膨胀为难以维护的"意大利面条式代码",任何新状态的加入或现有转换规则的调整都需要小心翼翼地修改那些冗长的判断块,这严重违反了开闭原则。状态模式正是为了消除庞大的条件分支语句将状态相关的行为局部化使状态转换显式化而生。

本文将以一条完整的知识脉络展开:我们将从最原始的 if-else 状态判断代码出发,经历经典状态模式的重构过程,探讨状态转换策略的权衡与状态的单例化、持久化等进阶特性。随后,我们将目光投向工业级框架的源码,剖析 JDK、Spring StateMachine、MyBatis、Netty 以及工作流引擎中对状态模式的精妙运用。更进一步,我们会深入分布式环境,探讨基于 Redis 和 ZooKeeper 的分布式状态机实现、Saga 事务中的状态管理。最后,通过五个典型场景的完整实战 Demo 与十余道专家级面试题解析,帮助读者完成从"会用"到"精通"的跃迁。


一、模式定义与结构

1.1 GoF 标准定义

状态模式允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。其核心思想是将与特定状态相关的行为封装在一个独立的状态对象中,上下文对象持有一个指向当前状态对象的引用,并将所有与状态相关的请求委托给该对象处理。

1.2 UML 类图

classDiagram class Context { - state: State + request() + setState(State) } class State { <> + handle(Context) } class ConcreteStateA { + handle(Context) } class ConcreteStateB { + handle(Context) } Context --> State : maintains State <|.. ConcreteStateA : implements State <|.. ConcreteStateB : implements

1.3 类图文字说明

在上面的类图中,Context 代表上下文对象,它是客户端实际交互的入口,内部维护了一个对抽象状态 State 的引用 state。当客户端调用 Contextrequest() 方法时,Context 并不会亲自处理业务逻辑,而是将调用委托 给当前持有的 State 对象:state.handle(this)。这种委托机制正是状态模式行为切换的核心------Context 自身的行为会随着 state 实例的不同而动态变化,仿佛在运行时修改了自身的类。

值得注意的是方法签名中的 Context 参数。handle(Context) 方法接收 Context 对象作为参数,这使得状态对象拥有了访问和修改上下文的能力。一方面,状态对象可以通过 Context 的公共方法读取必要的数据以完成业务逻辑;另一方面,更为关键的是,当业务执行完毕后满足了状态迁移条件,状态对象可以调用 context.setState(newState)主动触发状态转换。这种"状态对象持有上下文引用并决定下一状态"的设计,将状态转换逻辑内聚在状态类内部,极大地提升了系统的灵活性与可维护性。

1.4 角色职责详解

  • State(抽象状态接口):声明了所有具体状态类必须实现的行为方法。这些方法代表了在特定状态下对象能够响应的操作。接口的定义应当覆盖该状态机中所有可能被触发的行为,即便某些状态并不支持某些操作(此时可选择抛出异常或空实现)。
  • ConcreteState(具体状态类) :实现了 State 接口,封装了在该特定状态下的具体行为逻辑。它持有对 Context 的引用(通常在方法参数中传入),在执行完自身逻辑后,若满足条件,可以负责决定并触发向下一状态的转换。每个 ConcreteState 只需要关注自己所在状态下的规则,极大降低了代码的耦合度。
  • Context(上下文) :定义了客户端感兴趣的接口,并维护一个 ConcreteState 的实例。Context 不关心状态的具体行为,它只负责将请求转发给当前状态对象。此外,Context 提供了 setState() 方法,供状态类在合适的时机调用以切换内部状态。

二、代码演进与实现

2.1 不使用模式的原始代码:条件分支地狱

考虑一个简单的订单处理场景,订单有三种状态:待支付、已支付、已发货。在未使用模式的情况下,代码通常充斥着 if-elseswitch-case

java 复制代码
/**
 * 不使用状态模式的反面教材
 */
class Order {
    // 状态常量
    public static final int PENDING_PAYMENT = 0;
    public static final int PAID = 1;
    public static final int SHIPPED = 2;
    public static final int COMPLETED = 3;

    private int state = PENDING_PAYMENT;

    // 支付操作
    public void pay() {
        if (state == PENDING_PAYMENT) {
            System.out.println("支付成功,订单状态变更为:已支付");
            state = PAID;
        } else if (state == PAID) {
            System.out.println("订单已支付,请勿重复支付");
        } else if (state == SHIPPED) {
            System.out.println("订单已发货,无法支付");
        }
    }

    // 发货操作
    public void ship() {
        if (state == PENDING_PAYMENT) {
            System.out.println("订单未支付,无法发货");
        } else if (state == PAID) {
            System.out.println("发货成功,订单状态变更为:已发货");
            state = SHIPPED;
        } else if (state == SHIPPED) {
            System.out.println("订单已发货,请勿重复发货");
        }
    }

    // 确认收货操作
    public void confirm() {
        if (state == SHIPPED) {
            System.out.println("确认收货,订单完成");
            state = COMPLETED;
        } else {
            System.out.println("当前状态无法确认收货");
        }
    }
}

// 客户端调用
public class WithoutStatePatternDemo {
    public static void main(String[] args) {
        Order order = new Order();
        order.pay();    // 支付成功
        order.ship();   // 发货成功
        order.pay();    // 订单已发货,无法支付
        order.confirm();// 确认收货
    }
}

问题分析

  1. 违反开闭原则 :如果增加一种新状态(如"退款中"),我们需要修改 pay()ship()confirm() 等所有方法中的 if-else 分支,极易引入 Bug。
  2. 代码臃肿:随着状态数量的增加,每个方法中的条件分支会线性增长,导致代码难以阅读和维护。
  3. 状态转换逻辑分散 :状态转换的规则散落在各个方法的 state = xxx 赋值语句中,想要梳理完整的订单状态流转图变得极其困难。

2.2 经典状态模式重构

接下来,我们使用标准状态模式对上述代码进行重构。

Step 1: 定义 State 接口

java 复制代码
// 订单状态接口,定义了状态相关的行为方法
interface OrderState {
    void pay(OrderContext context);      // 支付
    void ship(OrderContext context);     // 发货
    void confirm(OrderContext context);  // 确认收货
}

Step 2: 实现具体状态类

java 复制代码
// 待支付状态
class PendingPaymentState implements OrderState {
    @Override
    public void pay(OrderContext context) {
        System.out.println("支付处理中... 支付成功!");
        // 状态转换:待支付 -> 已支付
        context.setState(new PaidState());
    }

    @Override
    public void ship(OrderContext context) {
        System.out.println("错误:订单尚未支付,无法发货。");
    }

    @Override
    public void confirm(OrderContext context) {
        System.out.println("错误:订单尚未支付,无法确认收货。");
    }
}

// 已支付状态
class PaidState implements OrderState {
    @Override
    public void pay(OrderContext context) {
        System.out.println("提示:订单已支付,无需重复操作。");
    }

    @Override
    public void ship(OrderContext context) {
        System.out.println("发货处理中... 发货成功!");
        // 状态转换:已支付 -> 已发货
        context.setState(new ShippedState());
    }

    @Override
    public void confirm(OrderContext context) {
        System.out.println("错误:订单尚未发货,无法确认收货。");
    }
}

// 已发货状态
class ShippedState implements OrderState {
    @Override
    public void pay(OrderContext context) {
        System.out.println("错误:订单已发货,无法支付。");
    }

    @Override
    public void ship(OrderContext context) {
        System.out.println("提示:订单已发货,请勿重复操作。");
    }

    @Override
    public void confirm(OrderContext context) {
        System.out.println("确认收货成功!交易完成。");
        // 状态转换:已发货 -> 已完成
        context.setState(new CompletedState());
    }
}

// 已完成状态(终结状态)
class CompletedState implements OrderState {
    @Override
    public void pay(OrderContext context) {
        System.out.println("错误:订单已完成,无法操作。");
    }

    @Override
    public void ship(OrderContext context) {
        System.out.println("错误:订单已完成,无法操作。");
    }

    @Override
    public void confirm(OrderContext context) {
        System.out.println("提示:订单已完成。");
    }
}

Step 3: 定义 Context 类

java 复制代码
// 订单上下文,持有当前状态对象
class OrderContext {
    private OrderState currentState;

    public OrderContext() {
        // 初始状态:待支付
        this.currentState = new PendingPaymentState();
    }

    // 提供修改状态的方法,供具体状态类调用
    public void setState(OrderState state) {
        this.currentState = state;
        System.out.println(">>> 内部状态已切换为: " + state.getClass().getSimpleName());
    }

    // 以下方法将请求委托给当前状态对象处理
    public void pay() {
        currentState.pay(this);
    }

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

    public void confirm() {
        currentState.confirm(this);
    }
}

Step 4: 客户端测试

java 复制代码
public class StatePatternDemo {
    public static void main(String[] args) {
        OrderContext order = new OrderContext();
        
        System.out.println("=== 测试正常流程 ===");
        order.pay();      // 待支付 -> 已支付
        order.ship();     // 已支付 -> 已发货
        order.confirm();  // 已发货 -> 已完成
        
        System.out.println("\n=== 测试异常操作 ===");
        OrderContext order2 = new OrderContext();
        order2.ship();    // 尝试未支付发货 -> 错误
        order2.pay();     // 支付
        order2.pay();     // 尝试重复支付 -> 提示
    }
}

2.3 状态模式的进阶特性

a. 状态转换策略:由 Context 负责转换 vs 由状态类负责转换

策略 实现方式 耦合度 控制力 适用场景
由状态类负责 ConcreteStatehandle() 方法中调用 context.setState() 状态类依赖 Context 接口 分散但灵活,易于定义复杂分支 状态转换规则复杂、新状态频繁增加
由 Context 负责 状态方法返回一个状态标识或下一状态实例,Context 执行赋值 Context 依赖状态返回值 集中在 Context,便于统一管理监控 流程相对固定、需要中心化控制的场景

上述示例采用的是 由状态类负责转换 的策略。这种方式最符合状态模式的初衷------将状态相关的行为(包括何时转换)完全封装在状态类中。

b. 状态的单例化:使用枚举实现状态对象

当具体状态类没有内部状态(即无状态对象)时,每次状态切换都 new 一个新对象会造成不必要的内存开销。Java 枚举是实现单例状态对象的极佳载体。

java 复制代码
// 使用枚举实现状态模式,兼具单例与简洁性
enum OrderStateEnum implements OrderState {
    PENDING_PAYMENT {
        @Override
        public void pay(OrderContext context) {
            System.out.println("支付成功!");
            context.setState(PAID);
        }
        // ... 其他方法实现
    },
    PAID {
        @Override
        public void ship(OrderContext context) {
            System.out.println("发货成功!");
            context.setState(SHIPPED);
        }
        // ...
    },
    SHIPPED { /* ... */ },
    COMPLETED { /* ... */ };
}

c. 状态持久化

在实际生产环境中,订单状态必须持久化到数据库。通常的做法是,Context 中保存状态的字符串/整型标识,在从数据库加载数据时,通过工厂方法将持久化字段映射回具体状态实例。

java 复制代码
// 状态工厂
class OrderStateFactory {
    public static OrderState getState(String stateName) {
        switch (stateName) {
            case "PENDING": return new PendingPaymentState(); // 或返回枚举单例
            case "PAID": return new PaidState();
            // ...
            default: throw new IllegalArgumentException("未知状态");
        }
    }
}

// 在 Context 加载时重建状态
class PersistentOrderContext {
    private String stateValue; // 持久化字段
    private OrderState currentState;
    
    public void loadFromDb(String dbState) {
        this.stateValue = dbState;
        this.currentState = OrderStateFactory.getState(dbState);
    }
}

2.4 状态模式行为委托时序图

sequenceDiagram participant Client as 客户端 participant Context as OrderContext participant StateA as PendingPaymentState participant StateB as PaidState Client->>Context: pay() activate Context Context->>StateA: pay(this) activate StateA StateA-->>StateA: 执行待支付状态下的支付逻辑 StateA->>Context: setState(PaidState) deactivate StateA Context->>Context: 内部状态替换为 PaidState Context-->>Client: 返回结果 deactivate Context Note over Client,StateB: 后续操作将委托给新状态对象 Client->>Context: ship() activate Context Context->>StateB: ship(this) activate StateB StateB-->>StateB: 执行已支付状态下的发货逻辑 StateB->>Context: setState(ShippedState) deactivate StateB Context-->>Client: 返回结果 deactivate Context

时序图说明

上图清晰地描绘了状态模式中的核心委托机制状态切换机制 。在初始阶段,客户端(Client)调用上下文(OrderContext)的 pay() 方法。上下文对象并没有包含支付业务的具体代码,而是立即将调用委托 给其内部持有的当前状态对象 PendingPaymentState

注意在 StateA 的执行生命线中,除了完成打印日志等业务逻辑外,最关键的动作是它回调了 Context 提供的 setState() 方法,并将新的状态实例 PaidState 作为参数传入。这一步操作使得 Context 的内部状态引用发生了改变。当客户端紧接着再次调用 ship() 方法时,Context 再次执行委托,但此时消息的接收者已经变成了刚刚切换进来的 PaidState 对象。这种动态绑定机制正是状态模式"对象看起来似乎修改了它的类"这一魔力的来源------同一个 Context 对象的同一个方法调用,在不同时刻表现出了完全不同的行为。


三、源码级应用分析

3.1 JDK 案例

1. java.util.Iterator 在 ArrayList 中的状态管理

ArrayList 的内部迭代器 Itr 实现了 Iterator 接口,其内部维护了一个 expectedModCount 变量来管理迭代过程中的"状态"。ArrayList 本身有一个 modCount 记录结构修改次数。

java 复制代码
// ArrayList.Itr 源码片段
private class Itr implements Iterator<E> {
    int cursor;       
    int lastRet = -1; 
    int expectedModCount = modCount; // 初始化快照

    public E next() {
        checkForComodification(); // 检查状态是否一致
        // ...
    }

    final void checkForComodification() {
        // 若外部修改了列表,modCount 会增加,导致状态不匹配抛出异常
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

这里虽非典型的 GoF 状态模式类图结构,但其体现了状态控制行为 的核心思想:迭代器的行为(next/remove)依赖于其内部的"是否被并发修改"这一隐式状态。

2. java.lang.Thread 的状态转换

Thread 类内部定义了 State 枚举:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED。底层 JVM 通过本地方法调用与操作系统线程状态机交互,但在 Java 层面,Thread 的状态转换逻辑(如 start() 只能从 NEW 进入,wait()RUNNABLE 进入 WAITING)体现了状态模式中 状态约束行为 的特征。

java 复制代码
public synchronized void start() {
    if (threadStatus != 0) // 0 代表 NEW
        throw new IllegalThreadStateException();
    // ...
}

3. javax.faces.lifecycle.Lifecycle

JSF 框架的 Lifecycle 类管理着 Faces 请求处理的六个标准阶段:RESTORE_VIEW, APPLY_REQUEST_VALUES, PROCESS_VALIDATIONS, UPDATE_MODEL_VALUES, INVOKE_APPLICATION, RENDER_RESPONSELifecycle 实现类 LifecycleImpl 使用状态模式管理这些阶段间的流转,每个阶段对应特定的处理逻辑,且允许通过 PhaseListener 在阶段前后进行拦截。

3.2 Spring 框架深度剖析

1. Spring StateMachine:状态机框架的完整实现

Spring StateMachine 是状态模式在框架层面的集大成者。它不仅提供了状态(State)和行为(Action),还显式定义了事件(Event)转换(Transition)守卫(Guard) 等高级概念。

  • State: 对应状态模式中的 ConcreteState。
  • Transition: 封装了"源状态 -> 触发事件 -> 目标状态"的映射。
  • Event: 触发状态流转的信号。
  • Guard : 状态转换前的条件判断,类似于状态模式中 handle 方法里的 if 条件。
java 复制代码
@Configuration
@EnableStateMachine
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<States, Events> {
    @Override
    public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception {
        transitions
            .withExternal()
                .source(States.PENDING).target(States.PAID).event(Events.PAY)
                .and()
            .withExternal()
                .source(States.PAID).target(States.SHIPPED).event(Events.SHIP);
    }
}

2. Lifecycle 接口:Spring Bean 生命周期管理

Spring 的 Lifecycle 接口定义了 start(), stop(), isRunning() 方法。AbstractApplicationContext 内部维护了一个 LifecycleProcessor,在容器刷新和关闭时遍历所有 Lifecycle Bean 并驱动其状态变更。虽然 Lifecycle 本身未显式使用多态状态类,但 Bean 的行为(是否接受请求、是否处理任务)严格依赖于其 isRunning() 返回的状态,这同样体现了状态决定行为的设计哲学。

3.3 MyBatis 框架

Executor 执行器类型

MyBatis 的 Executor 接口有三个核心实现:SimpleExecutor(默认)、ReuseExecutor(重用 Statement)、BatchExecutor(批处理)。在 Configuration 中配置 defaultExecutorType 实际上决定了 SqlSession 在运行时处于何种"执行状态"。不同执行器对 update/query 的处理逻辑差异巨大,例如 BatchExecutor 会延迟 SQL 执行直到 flushStatements 被调用,这符合状态模式中不同状态不同行为的特征。

3.4 Netty 框架

ChannelHandler 中的 Channel 生命周期

Netty 的 ChannelHandler 定义了明确的生命周期回调方法:channelRegistered, channelActive, channelInactive, channelUnregistered。Netty 内部的 AbstractChannel 维护了一个状态变量,并在 I/O 事件发生时(如连接建立、读空闲、连接断开)修改状态,随后触发对应的 ChannelHandler 回调。

java 复制代码
// 简化的 Netty 状态流转示意
public interface ChannelHandler {
    // 当 Channel 注册到 EventLoop 时调用(状态变为 Registered)
    void channelRegistered(ChannelHandlerContext ctx) throws Exception;
    // 当 Channel 变为活跃时(状态变为 Active,可读写)
    void channelActive(ChannelHandlerContext ctx) throws Exception;
    // 当 Channel 变为非活跃时(连接断开)
    void channelInactive(ChannelHandlerContext ctx) throws Exception;
}

3.5 工作流引擎

Activiti / Flowable 流程实例状态

Activiti 引擎中的流程实例(ProcessInstance)运行时状态被严格建模为状态机。主要状态包括:Running(运行中)、Suspended(挂起)、Ended(结束)。当调用 runtimeService.suspendProcessInstanceById() 时,引擎会校验当前状态(只有 Running 可挂起),更新数据库中的状态字段,并且该实例将不再被 Job 执行器触发。这背后依赖的正是状态模式的思想------状态对象决定了流程实例对 API 调用的响应方式。


四、分布式环境下的状态模式

在微服务和云原生时代,状态不再局限于单个 JVM 内部,状态模式的设计思想被广泛应用于分布式协调、事务一致性及配置管理等场景。

4.1 分布式状态机的实现

当状态机需要在多节点间同步时,需要引入一个集中式存储(如 ZooKeeper、etcd 或 Redis)来维护全局状态。

  • ZooKeeper:利用其临时节点和 Watcher 机制。状态变更方更新 ZNode 数据,依赖方监听该 ZNode 变化,收到通知后拉取新状态并执行本地逻辑。
  • etcd:利用其 Watch API 和 MVCC 机制,实现类似功能,常用于 Kubernetes Operator 模式中的状态调谐(Reconcile)。

4.2 Saga 分布式事务中的状态管理

在 Saga 模式中,全局事务由多个本地事务组成,每个本地事务对应一个参与者的状态变更。协调者维护一个全局状态机(如:订单创建中 -> 库存扣减中 -> 支付处理中 -> 完成/补偿中)。当某个参与者失败时,协调者驱动状态机进入"补偿"路径,各参与者根据当前状态执行对应的回滚逻辑(即状态模式中的不同行为)。

4.3 订单系统的分布式状态流转

一个典型的电商微服务架构中,订单状态可能分布在订单服务、支付服务、物流服务中。

  1. 支付服务 收到回调,更新本地支付流水状态为"已支付"。
  2. 支付服务 发送 OrderPaidEvent 消息至 Kafka/RocketMQ。
  3. 订单服务 监听到事件,调用 OrderContext.pay() 方法,状态模式执行"待支付 -> 已支付"的转换逻辑,并持久化。
  4. 若订单服务宕机,消息会重试,利用幂等性设计保证状态最终一致。

4.4 工作流引擎的分布式状态持久化

Flowable 和 Camunda 均将流程实例的状态(执行指针位置、变量值)持久化到关系型数据库(如 MySQL、PostgreSQL)的 ACT_RU_EXECUTION 表中。当流程流转时,引擎执行 UPDATE 语句更新状态字段。这允许多个流程引擎节点共享同一个数据库,任何一个节点宕机,其他节点可以从数据库中读取状态继续推进流程,实现了状态的分布式高可用。

4.5 配置中心的状态变更监听

Apollo 和 Nacos 允许客户端监听配置变更。客户端内部通常维护着基于配置项的 Bean 或组件。当配置中心推送变更时,客户端应用会触发热刷新逻辑。这类似于状态模式中的 setState 操作:应用的行为从"旧配置行为"切换到"新配置行为"。

4.6 基于 Redis 的分布式状态机示例

以下是一个简化的分布式订单状态机实现,使用 Redis 存储状态,利用 Redisson 的分布式锁保证状态转换的原子性。

java 复制代码
@Service
public class DistributedOrderStateMachine {
    private final RedissonClient redisson;
    private static final String STATE_KEY_PREFIX = "order:state:";

    public void handleEvent(String orderId, OrderEvent event) {
        String key = STATE_KEY_PREFIX + orderId;
        RLock lock = redisson.getLock(key + ":lock");
        try {
            lock.lock();
            String currentStateStr = redisson.getBucket(key).get();
            OrderState currentState = OrderStateFactory.getState(currentStateStr);
            // 状态模式委托
            OrderContext context = new OrderContext(currentState, orderId);
            context.handle(event); 
            // 更新 Redis 状态
            redisson.getBucket(key).set(context.getCurrentState().name());
        } finally {
            lock.unlock();
        }
    }
}

4.7 分布式状态机架构图

flowchart TD subgraph Service_A [订单服务节点 A] A1[接收到支付成功事件] --> A2[获取分布式锁] A2 --> A3[读取 Redis 当前状态: 待支付] A3 --> A4[委托状态对象处理: PendingState.pay] A4 --> A5[状态变更: 已支付] A5 --> A6[写回 Redis 并释放锁] end subgraph Redis [共享存储 Redis] R1[("订单状态: 已支付")] R2[("分布式锁")] end subgraph Service_B [库存服务节点 B] B1[监听 Redis Key 变更] B1 --> B2[感知状态变更为已支付] B2 --> B3[执行库存扣减逻辑] end A6 --> R1 A2 -.-> R2 B1 -.-> R1

架构图说明

该流程图展示了一个典型的利用 Redis 作为共享存储的分布式状态机架构。整个流程分为三个核心区域:订单服务节点 A、Redis 共享存储层、库存服务节点 B。

  1. 状态变更与持久化 :服务节点 A 在处理外部事件(如支付回调)时,首先通过分布式锁(Redisson)获取对特定订单状态 Key 的独占权,防止并发冲突。随后从 Redis 读取当前状态快照,利用本地状态模式(PendingState.pay())执行业务逻辑。一旦状态对象决定转换,节点 A 不仅会更新本地 Context 引用,还会同步地将新状态写回 Redis,实现状态的集中持久化。
  2. 状态感知与行为驱动 :服务节点 B(例如下游的库存服务)无需与服务 A 进行直接的 RPC 通信。它可以通过 Redis 的 Keyspace Notifications 或简单的定时轮询来监听特定 Key 的变化。当感知到订单状态由"待支付"变为"已支付"时,节点 B 触发相应的业务行为(如锁定库存)。这种设计解耦了服务间的直接依赖,实现了基于状态的最终一致性协同工作。

五、对比辨析

5.1 状态模式 vs 策略模式

对比维度 状态模式 (State) 策略模式 (Strategy)
类图结构 高度相似,均为 Context 聚合接口,多态实现 高度相似
核心意图 让对象在不同内部状态下表现出不同行为 让对象在不同算法策略下表现出不同行为
转换驱动 内部驱动:状态对象自己知道何时、为何转换到另一个状态 外部驱动:客户端显式指定或根据配置注入策略,策略本身不决定切换
关注点 状态变迁的规则封装 算法的互换性与扩展性
生命周期 Context 通常持有状态对象的引用,状态会频繁变化 Context 通常持有固定策略,或者按需切换,切换频率低

5.2 状态模式 vs 有限状态机(FSM)

有限状态机(FSM)是一种数学模型,包含状态、事件、转换和动作。状态模式是实现 FSM 的一种面向对象的、优雅的方式。在状态模式中:

  • State 类 = FSM 的状态节点。
  • Context 的 setState = FSM 的转换动作。
  • State 类中的方法 = FSM 中对事件的响应(Action)。

5.3 状态模式 vs 责任链模式

  • 状态模式 :关注"在当前状态下,能做什么"。请求总是由当前这一个状态对象处理。
  • 责任链模式 :关注"这个请求谁来处理"。请求沿着链条传递,直到被某一个处理器处理或到达链尾。

5.4 状态模式 vs 观察者模式

两者可以结合使用。当 Context 的状态发生改变时(setState 被调用),可以触发观察者模式通知监听器(Listeners)。但观察者模式的核心是一对多的依赖通知 ,而状态模式的核心是封装状态行为

5.5 状态模式 vs 命令模式

  • 命令模式 :将请求封装为对象,侧重于请求的发送者与接收者解耦,支持撤销、重做。
  • 状态模式 :将状态相关行为封装为对象,侧重于状态流转与行为切换。
  • 联系:在状态模式中,客户端对 Context 的调用可以看作发送了一个命令,具体执行取决于状态。

六、适用场景分析(重点强化)

场景一:订单状态流转系统

Demo 代码

java 复制代码
// 上下文
class OrderContext {
    private OrderState state;
    public OrderContext() { this.state = new PendingState(); }
    public void setState(OrderState state) { this.state = state; }
    public void pay() { state.pay(this); }
    public void cancel() { state.cancel(this); }
    public void refund() { state.refund(this); }
    public void ship() { state.ship(this); }
    public void confirm() { state.confirm(this); }
}

// 状态接口
interface OrderState {
    void pay(OrderContext ctx);
    void cancel(OrderContext ctx);
    void refund(OrderContext ctx);
    void ship(OrderContext ctx);
    void confirm(OrderContext ctx);
}

// 具体状态实现(仅展示关键转换逻辑)
class PendingState implements OrderState {
    public void pay(OrderContext ctx) {
        System.out.println("[待支付] 支付成功");
        ctx.setState(new PaidState());
    }
    public void cancel(OrderContext ctx) {
        System.out.println("[待支付] 取消订单");
        ctx.setState(new CancelledState());
    }
    // ... 其他方法抛异常或空实现
}

class PaidState implements OrderState {
    public void refund(OrderContext ctx) {
        System.out.println("[已支付] 退款处理中...");
        ctx.setState(new RefundingState());
    }
    public void ship(OrderContext ctx) {
        System.out.println("[已支付] 发货成功");
        ctx.setState(new ShippedState());
    }
    // ...
}

Mermaid 类图

classDiagram class OrderContext { -state: OrderState +pay() +cancel() +refund() +ship() } class OrderState { <> +pay(ctx) +cancel(ctx) +refund(ctx) +ship(ctx) } class PendingState { +pay(ctx): -> PaidState +cancel(ctx): -> CancelledState } class PaidState { +refund(ctx): -> RefundingState +ship(ctx): -> ShippedState } class ShippedState { } class CompletedState { } class CancelledState { } OrderContext --> OrderState OrderState <|.. PendingState OrderState <|.. PaidState OrderState <|.. ShippedState OrderState <|.. CompletedState OrderState <|.. CancelledState

文字说明

该设计通过将状态转换规则显式地编写在具体状态类的方法中(例如 PendingState.pay() 内部调用 ctx.setState(new PaidState())),实现了状态转换规则的封装 。任何尝试执行非法状态转换的请求(例如在 PendingState 下调用 refund())都会在对应状态类的方法中抛出异常或返回错误提示,从而在运行时防止非法状态转换 。这种设计极大地增强了系统的健壮性:新增一个"退款中"状态时,开发者只需新建 RefundingState 类,并在 PaidStateShippedStaterefund 方法中添加跳转逻辑即可,完全无需修改订单上下文主类或其他已有状态类,完美遵循开闭原则。


场景二:TCP 连接状态管理

Demo 代码(模拟)

java 复制代码
class TcpConnection {
    private TcpState state;
    public TcpConnection() { this.state = new ClosedState(); }
    void setState(TcpState state) { this.state = state; }
    public void open() { state.open(this); }
    public void close() { state.close(this); }
    public void send(String data) { state.send(this, data); }
}

interface TcpState {
    void open(TcpConnection conn);
    void close(TcpConnection conn);
    void send(TcpConnection conn, String data);
}

class ClosedState implements TcpState {
    public void open(TcpConnection conn) {
        System.out.println("连接已建立,进入 LISTEN 状态");
        conn.setState(new ListenState());
    }
    // close 和 send 均不可用
}
class ListenState implements TcpState { /* ... */ }
class EstablishedState implements TcpState {
    public void send(TcpConnection conn, String data) {
        System.out.println("发送数据: " + data);
    }
    public void close(TcpConnection conn) {
        System.out.println("发起主动关闭,进入 FIN_WAIT_1 状态");
        conn.setState(new FinWait1State());
    }
}

Mermaid 状态图(Flowchart)

flowchart LR CLOSED -->|主动打开/SYN| SYN_SENT CLOSED -->|被动打开| LISTEN LISTEN -->|收到SYN/发送SYN+ACK| SYN_RCVD SYN_SENT -->|收到SYN+ACK/发送ACK| ESTABLISHED SYN_RCVD -->|收到ACK| ESTABLISHED ESTABLISHED -->|主动关闭/FIN| FIN_WAIT_1 ESTABLISHED -->|被动关闭/收到FIN| CLOSE_WAIT FIN_WAIT_1 -->|收到ACK| FIN_WAIT_2 FIN_WAIT_1 -->|收到FIN+ACK| TIME_WAIT FIN_WAIT_2 -->|收到FIN/发送ACK| TIME_WAIT CLOSE_WAIT -->|应用关闭/发送FIN| LAST_ACK LAST_ACK -->|收到ACK| CLOSED TIME_WAIT -->|超时| CLOSED

文字说明

TCP 协议的状态机是计算机网络中最经典的复杂状态机之一,包含 11 种状态。如果使用传统的 switch-case 来处理 socket 事件(如收到 SYN、收到 ACK、应用关闭),代码将极其晦涩难懂。状态模式将复杂的协议状态图映射为类的层次结构 ,每一个状态类只关注在该状态下收到特定报文时应执行的逻辑以及应跳转的目标状态。例如,EstablishedState 类只关心 send(传输数据)和 close(主动关闭)操作。当底层网络收到一个 FIN 报文时,EstablishedState 的处理逻辑会无缝地将连接状态迁移至 CloseWaitState。这种设计使得代码结构与 RFC 文档中的状态转换图高度一致,极大地降低了维护和理解复杂网络协议的认知负担。


场景三:游戏角色状态系统

Demo 代码(含生命周期钩子)

java 复制代码
abstract class RoleState {
    protected Role context;
    public RoleState(Role context) { this.context = context; }
    // 生命周期钩子
    public void onEnter() {}
    public void onExit() {}
    public void onUpdate() {} // 每帧/每回合调用
    
    public abstract void attack();
    public abstract void move();
}

class PoisonedState extends RoleState {
    private int duration = 3;
    public PoisonedState(Role context) { super(context); }
    
    @Override public void onEnter() { System.out.println("角色中毒了!"); }
    
    @Override public void onUpdate() {
        context.setHp(context.getHp() - 5);
        System.out.println("中毒伤害 -5, 剩余HP: " + context.getHp());
        if (--duration <= 0) {
            context.setState(new NormalState(context));
        }
    }
    
    @Override public void attack() { 
        System.out.println("中毒状态下攻击力减半!造成 10 点伤害"); 
    }
    // ... move 实现
}

Mermaid 时序图

sequenceDiagram participant Enemy as 敌人 participant Role as 角色 participant State as 当前状态(Normal) participant NewState as 中毒状态(Poisoned) Enemy->>Role: 攻击(毒素) Role->>State: onExit() Note over State: 清理正常状态资源 Role->>Role: setState(Poisoned) Role->>NewState: onEnter() Note over NewState: 显示中毒特效 loop 持续3回合 Role->>NewState: onUpdate() NewState-->>Role: 扣除生命值 alt 生命值归零 Role->>Role: setState(Dead) end end Role->>NewState: onExit() Role->>Role: setState(Normal) Note over Role: 恢复正常状态

文字说明

在游戏开发中,角色常常会受到多种 Buff/Debuff 的影响(如同时处于"中毒"和"眩晕")。经典状态模式是单状态机,难以直接处理状态叠加。解决方案有两种:一是引入状态集合(State Set) ,即 Role 维护一个 List<RoleState> 而不是单一引用,每帧遍历所有状态执行 onUpdate;二是引入状态层(State Layer) 概念,基础状态(移动、攻击)和效果状态(Buff)分离。上图时序图展示了单状态切换的标准流程,重点强调了 onEnteronExit 钩子的重要性:在进入中毒状态时,可以通过 onEnter 改变角色贴图颜色或播放粒子特效;在退出时通过 onExit 清理特效。这种生命周期管理保证了状态切换时的资源正确释放和界面刷新。


场景四:ATM 取款机流程

Demo 代码

java 复制代码
class AtmContext {
    private AtmState state = new IdleState();
    private int cashInMachine = 10000;
    // getters/setters...
    public void insertCard() { state.insertCard(this); }
    public void inputPin(String pin) { state.inputPin(this, pin); }
    public void withdraw(int amount) { state.withdraw(this, amount); }
    public void ejectCard() { state.ejectCard(this); }
}

class IdleState implements AtmState {
    public void insertCard(AtmContext atm) {
        System.out.println("卡片已插入,请输入密码");
        atm.setState(new PinValidationState());
    }
    // 其他操作无效...
}
class PinValidationState implements AtmState {
    private int attempts = 0;
    public void inputPin(AtmContext atm, String pin) {
        if ("1234".equals(pin)) {
            atm.setState(new OperationSelectionState());
        } else if (++attempts >= 3) {
            System.out.println("密码错误超过3次,吞卡!");
            atm.setState(new CardRetainedState());
        }
    }
}

Mermaid 流程图

flowchart TD Start([开始]) --> Idle[空闲状态] Idle -->|插卡| Pin[密码验证状态] Pin -->|密码正确| Select[选择操作状态] Pin -->|密码错误3次| Retain[吞卡状态] Select -->|选择取款| Withdraw[出钞状态] Withdraw -->|余额充足| Dispense[吐钞并退卡] Withdraw -->|余额不足| Error[显示错误返回选择] Select -->|选择退卡| Eject[退卡状态] Eject --> Idle Retain --> Idle

文字说明

ATM 是教学状态模式的经典案例,因为其流程具有严格的顺序性和互斥性。在没有卡时绝对不能取款,在未验证密码时不能查询余额。上述流程图清晰地描绘了用户操作(插卡、输密码、选操作)如何驱动 ATM 内部状态机的流转。状态模式在这种场景下的最大价值在于对异常路径的集中管理 。例如,超时处理:可以在 PinValidationStateonEnter 钩子中启动一个定时器,若 30 秒内无操作,自动调用 context.setState(new IdleState()) 并退卡。又如多次输错密码导致的吞卡逻辑,完全被封装在 PinValidationState 内部,不会污染 IdleStateOperationSelectionState 的代码。这使得 ATM 控制程序在面对严格的安全审计和频繁的需求变更(如增加"手机取款"状态)时,依然保持高度的稳定性和可扩展性。


场景五:文档审批流程

Demo 代码

java 复制代码
class LeaveRequestContext {
    private ApprovalState state = new DraftState();
    private String applicant;
    public void submit() { state.submit(this); }
    public void approve(String approver) { state.approve(this, approver); }
    public void reject(String approver) { state.reject(this, approver); }
    // ...
}

class DeptManagerApprovalState implements ApprovalState {
    public void approve(LeaveRequestContext ctx, String approver) {
        if ("部门经理".equals(approver)) {
            System.out.println("部门经理审批通过,流转至 HR");
            ctx.setState(new HrApprovalState());
        }
    }
}

Mermaid 流程图

flowchart LR subgraph 申请人 A[创建草稿] --> B[提交申请] end subgraph 审批链 B --> C{部门经理审批} C -->|通过| D{HR审批} C -->|驳回| E[草稿状态] D -->|通过| F[完成归档] D -->|驳回| E end

文字说明

传统的审批流程通常通过硬编码的 if (status == 1 && role == MANAGER) 来实现,若需增加"财务审批"节点,必须在原有的 if-else 链条中插入新分支,风险极高。采用状态模式后,新增审批节点转化为新增一个状态类 。例如要新增"财务审批",只需创建 FinanceApprovalState 类,并修改 DeptManagerApprovalStateapprove 方法,将目标状态从 HrApprovalState 改为 FinanceApprovalState(或者更好的是,由流程定义配置驱动)。这种方式符合开闭原则:对扩展开放(增加新状态类),对修改封闭(无需改动旧状态类内部逻辑)。结合数据库持久化,可以实现流程的灵活配置化。


七、面试题精选与专家级解答

1. 状态模式和策略模式的本质区别?

:虽然类图高度一致,但意图转换机制 完全不同。状态模式的状态转换是由状态对象内部驱动 的,状态类知道自己何时应该流转到下一个状态(例如 OrderState.pay() 内部调用 context.setState(new PaidState()))。而策略模式的策略是由客户端外部注入的,策略类本身不关心何时被替换,也不负责选择下一个策略。在实际开发中,如果对象的行为变化是由内部状态触发的连续变迁,用状态模式;如果是根据配置或输入选择不同的算法处理,用策略模式。

2. 状态模式中如何实现状态的持久化与恢复?

  • 方案一:保存状态枚举名称 。在数据库中存储当前状态的字符串(如 "PAID"),加载时通过 StateFactory 映射为具体状态实例。这是最常用、最简单的方案。
  • 方案二:序列化状态对象 。将整个 Context 或其状态对象序列化为二进制或 JSON 存入数据库。适用于状态类包含较多运行时动态数据(如 duration 计时器)的场景,但需注意反序列化时的版本兼容性。
  • 方案三:事件溯源(Event Sourcing) 。不直接存储当前状态,而是存储历史上发生的所有状态变更事件(OrderPaidEvent, OrderShippedEvent)。恢复时通过重放(Replay)事件来重建当前状态。适用于需要审计日志和高可靠性溯源的金融系统。

3. 状态模式如何避免 Context 类过于臃肿?

  1. 职责分离Context 仅负责状态持有和委托,业务数据(如订单金额、商品列表)应放在独立的实体对象中,Context 持有该实体引用或仅传递必要参数。
  2. 使用抽象基类 :如果状态过多,可抽取 AbstractState 提供默认抛出异常的实现,具体状态类仅覆写自己关心的合法操作,减少冗余代码。
  3. 状态机框架化 :当状态超过 10 个或转换逻辑极其复杂时,手动维护 setState 调用会变得困难,应引入 Spring StateMachine 或自研 DSL 状态机,将状态拓扑配置化。

4. JDK 线程状态转换如何体现状态模式?

Thread.State 枚举定义了六个状态。虽然 JVM 底层是 C++ 的状态机,但在 Java 层面,Thread 类的行为受其 threadStatus 严格控制。例如,只有处于 NEW 状态的线程调用 start() 才会成功,否则抛出 IllegalThreadStateException;处于 RUNNABLE 的线程调用 wait() 会进入 WAITING。这体现了状态决定对象接口调用的合法性 ,是状态模式的核心思想。Thread 本身充当了 Context 角色,而本地代码中的状态机逻辑充当了 ConcreteState

5. Spring StateMachine 与手工实现的状态模式相比有哪些增强?

  • 配置化 :通过 StateMachineConfigurer 可以将状态流转拓扑(Source-Target-Event)声明式配置,无需在状态类中硬编码 setState 调用。
  • 事件驱动 :明确引入了 Event 概念,将"发生了什么"和"状态怎么变"解耦。
  • 守卫与动作 :提供了 Guard(条件判断)和 Action(状态进入/退出时的副作用)的标准扩展点。
  • 持久化与分布式 :内置了 StateMachinePersister 支持将状态机状态持久化到数据库,并支持基于 ZooKeeper 的分布式状态机协调。

6. 分布式系统中如何利用状态模式设计高可用分布式状态机?

  1. 状态存储:使用强一致性存储(如 etcd、ZooKeeper、Redis Redlock)保存当前状态。
  2. 锁机制:状态变更前必须获取分布式锁,防止并发更新导致状态覆盖。
  3. 事件驱动协同:状态变更后发布领域事件到消息队列,下游服务监听事件执行本地操作(Saga 模式)。
  4. 故障恢复 :节点宕机后重启,通过读取共享存储中的状态快照重建本地 Context,继续提供服务。

7. 状态转换逻辑放在 Context 中还是状态类中?

策略 优点 缺点
状态类负责转换 高度内聚,符合单一职责;新增状态无需修改 Context 状态类之间产生耦合(需知道下一状态是谁)
Context 负责转换 状态类完全解耦,可视为无状态的策略;便于统一监控和拦截 Context 会变成巨大的 switch-case,重回反模式

专家建议 :在绝大多数业务场景下,推荐由状态类负责转换。只有状态转换逻辑极其简单且固定时,才可考虑由 Context 根据状态返回值统一管理。

8. 如何用枚举实现状态模式?有何优劣?

:Java 枚举天生是单例且支持方法覆写。

java 复制代码
enum OrderEnumState {
    PENDING {
        void pay(OrderContext ctx) { ctx.state = PAID; }
    },
    PAID {
        void ship(OrderContext ctx) { ctx.state = SHIPPED; }
    };
    abstract void pay(OrderContext ctx);
}

优势 :代码极简、JVM 保证单例、序列化安全、内存占用小。 限制:无法继承、状态不能持有非静态变量(枚举实例全局唯一,多订单共享会导致数据串扰)。适用于无状态(或仅依赖 Context 传入数据)的状态机。

9. 状态模式与责任链模式能否结合使用?

:可以结合。在审批工作流中,链上的每个节点(Handler)负责审批逻辑,而审批单本身具有状态(待审批、已通过、已驳回)。审批节点执行后,会驱动审批单的状态发生变更。这是一种典型的 责任链驱动状态流转 的混合模式。

10. 如何处理状态进入和退出时的附加操作?

:在 State 接口中定义 onEnter(Context)onExit(Context) 生命周期钩子。在 Context.setState(State newState) 方法内部:

  1. currentState != null,调用 currentState.onExit(this)
  2. 设置 currentState = newState
  3. 调用 newState.onEnter(this)

利用钩子可以优雅地实现:记录状态变更日志、触发监控埋点、申请/释放资源(如连接池)、发送通知消息等横切关注点,避免业务代码污染。


八、总结

状态模式是面向对象设计中解决复杂条件分支和状态流转问题的利器。从最简单的 if-else 重构,到 JDK 线程模型、Spring 状态机框架、再到分布式微服务架构下的 Saga 事务协调,状态模式及其变体无处不在。掌握状态模式不仅是掌握一种设计模式,更是掌握了将复杂的生命周期管理转化为清晰的状态拓扑图的系统思维能力。希望本文能帮助你在日常开发与系统架构设计中,更加游刃有余地驾驭"状态"这一核心概念。

相关推荐
我的征途是星辰大海。2 小时前
设计模式(学习笔记)(第一章)
笔记·学习·设计模式
敖正炀2 小时前
创建型模式-抽象工厂模式
设计模式
敖正炀2 小时前
创建型模式-工厂方法模式
设计模式
敖正炀2 小时前
创造型模式-单例模式
设计模式
ximu_polaris3 小时前
设计模式(C++)-结构型模式-组合模式
c++·设计模式·组合模式
geovindu4 小时前
go: Singleton Pattern
单例模式·设计模式·golang
ximu_polaris4 小时前
设计模式(C++)-结构型模式-外观模式
c++·设计模式·外观模式
不知名的老吴4 小时前
思考:设计模式对前端有用吗?
设计模式·状态模式
ximu_polaris4 小时前
设计模式(C++)-创造型模式-建造者模式
c++·设计模式·建造者模式