深入浅出设计模式【二十、状态模式】
一、状态模式介绍
在软件开发中,经常会遇到对象的行为依赖于其内部状态的情况。一个典型的例子是订单系统:订单可能有“未支付”、“已支付”、“已发货”、“已完成”、“已取消”等状态。订单的可用操作(如支付、发货、确认收货、取消)以及这些操作的结果,都取决于订单当前所处的状态。
一种直观的实现方式是在对象的方法(如 pay(), ship(), cancel())中使用大量的 if-else 或 switch-case 语句来判断当前状态并执行相应的逻辑。这种方式的缺点是:
- 违反开闭原则 (OCP): 增加新状态或修改状态行为需要修改现有方法。
- 代码臃肿复杂: 状态判断逻辑充斥在多个方法中,难以理解和维护。
- 状态转换不清晰: 状态间的转换规则被硬编码在各个方法里,分散且容易出错。
- 僵化脆弱: 对某一状态的修改可能无意中影响其他状态的行为。
状态模式通过将对象的各种状态封装成独立的状态类,并将与状态相关的行为委托给代表当前状态的对象,巧妙地解决了上述问题。当对象的状态改变时,只需简单地切换它所持有的状态对象(改变委托对象),行为便随之改变。
二、核心概念与意图
-
核心概念:
- 上下文 (Context): 定义客户端感兴趣的接口。维护一个具体状态对象 (
ConcreteState) 的实例,这个实例定义了当前的状态。上下文将状态相关的行为委托给当前状态对象。上下文通常也提供一个设置状态的方法(setState())。 - 状态接口/抽象状态 (State): 定义一个接口或抽象类,用于封装与上下文特定状态相关的行为。
- 具体状态 (Concrete State): 实现状态接口。每一个具体状态类实现一个与上下文状态相关的行为。每个具体状态类都知道在状态转换时如何将自己切换到其他状态(通常通过调用上下文对象的
setState()方法)。
- 上下文 (Context): 定义客户端感兴趣的接口。维护一个具体状态对象 (
-
意图:
- 允许一个对象在其内部状态改变时改变它的行为。
- 将与特定状态相关的行为局部化,并且将状态转换显式化。
- 消除庞大的条件分支语句,使代码更清晰、可维护且易于扩展。
三、适用场景剖析
状态模式在以下场景中非常有效:
- 对象的行为取决于其状态,并且该对象需要在运行时根据状态改变行为: 这是状态模式的本质诉求,如订单、工单、审批流程、游戏角色、电梯控制等。
- 一个操作中含有庞大的、依赖于状态的多分支条件语句(
if-else或switch-case),这些分支对应着对象的各种状态: 状态模式通过将这些分支分散到独立的State子类中,消除了这些条件分支。 - 状态转换规则相对明确且有限,但状态数量可能较多: 状态模式显式定义了状态转换的规则(通常在具体状态类的行为方法中实现)。
- 需要轻松地添加新状态: 符合开闭原则,添加新状态只需要创建一个新的
ConcreteState类并修改相关的转换点(通常只需修改一到两个已有的状态类),无需修改上下文或所有使用状态的位置。
四、UML 类图解析(Mermaid)
以下UML类图清晰地展示了状态模式的结构、角色间的关系以及状态转换的流程:
Context(上下文):- 持有当前状态对象的引用 (
-state: State)。 - 定义客户端接口 (
+request()) 或其他依赖状态的行为。在这些方法内部,通常会将调用委托给当前状态对象的相应方法(如state.handle(this))。 - 提供一个用于改变内部状态的方法 (
+setState(state: State)),供状态对象在需要转换状态时调用。 - 可以包含业务操作所需的其他数据(如订单ID、金额等),状态对象在
handle()方法中可能需要访问这些数据。
- 持有当前状态对象的引用 (
State(状态接口/抽象状态):- 定义了一个所有具体状态都应实现的接口(如
handle(context: Context))。 - 该接口的方法参数通常包含一个对上下文对象的引用 (
context: Context),使得具体状态对象能够访问上下文的数据,并能在其行为方法内部调用上下文的方法(尤其是setState()来触发状态转换)。
- 定义了一个所有具体状态都应实现的接口(如
ConcreteStateA,ConcreteStateB(具体状态):- 实现
State接口。 - 在实现的
handle(context: Context)(或更明确的如pay(context),ship(context)) 方法中:- 执行与自身状态相关的业务逻辑。
- 根据逻辑结果和业务规则,决定是否需要进行状态转换。如果需要转换,则调用
context.setState(new ConcreteStateX())来改变上下文的状态对象。
- 关键: 具体状态类负责自己后续应该转换到哪个状态。这是状态转换规则的核心所在。
- 实现
- 状态转换流程:
- 客户端对
Context对象发起请求(如调用context.request())。 Context在其request()方法内部,将请求委托给其当前持有的State对象(即state.handle(context))。- 当前的
ConcreteState对象执行与该请求相关的业务逻辑。 - 在执行逻辑的过程中,如果需要改变状态,该
ConcreteState对象会调用context.setState(newState)方法。 - 在
setState(newState)方法内:- 上下文改变其
state引用,指向新的ConcreteState对象 (this.state = newState)。 - 可能会执行一些伴随状态改变的必要操作(如通知监听器、记录日志等)。
- 上下文改变其
- 下一次客户端再调用
context.request()时,委托将发给新的ConcreteState对象,行为也随之改变。
- 客户端对
五、各种实现方式及其优缺点
状态模式的实现关键在于状态接口的设计和状态转换规则的管理。
1. 标准实现(接口 + 具体状态类)
即上述UML所描述的方式,每个状态一个具体类,状态转换由状态对象触发。
// 1. State Interface
public interface OrderState {
void pay(OrderContext context);
void ship(OrderContext context);
void cancel(OrderContext context);
// ... other state-specific actions
}
// 2. Concrete States
public class UnpaidState implements OrderState {
@Override
public void pay(OrderContext context) {
System.out.println("Payment received. Thank you!");
context.setState(new PaidState()); // State transition triggered by the state object
}
@Override
public void ship(OrderContext context) {
System.out.println("ERROR: Cannot ship unpaid order.");
}
@Override
public void cancel(OrderContext context) {
System.out.println("Order cancelled.");
context.setState(new CancelledState()); // State transition
}
}
public class PaidState implements OrderState {
@Override
public void pay(OrderContext context) {
System.out.println("ERROR: Order already paid.");
}
@Override
public void ship(OrderContext context) {
System.out.println("Order shipped!");
context.setState(new ShippedState()); // State transition
}
@Override
public void cancel(OrderContext context) {
System.out.println("Cancelling paid order. Issuing refund.");
context.setState(new CancelledState()); // State transition
}
}
// ... Other ConcreteStates: ShippedState, CancelledState
// 3. Context (Order)
public class OrderContext {
private OrderState currentState;
public OrderContext() {
this.currentState = new UnpaidState(); // Initial state
}
public void setState(OrderState newState) {
this.currentState = newState;
System.out.println("State changed to: " + newState.getClass().getSimpleName());
// Potentially: notify observers, log, etc.
}
// Client-facing operations delegate to the current state
public void pay() {
currentState.pay(this);
}
public void ship() {
currentState.ship(this);
}
public void cancel() {
currentState.cancel(this);
}
}
// 4. Client
public class Client {
public static void main(String[] args) {
OrderContext order = new OrderContext();
order.pay(); // Output: Payment received... -> State changed to PaidState
order.ship(); // Output: Order shipped! -> State changed to ShippedState
order.ship(); // Output: ERROR: Order already shipped. (Assuming ShippedState handles ship() accordingly)
}
}
- 优点:
- 消除条件分支: 将与状态相关的行为清晰地分离到不同的状态类中。
- 遵循单一职责原则 (SRP): 每个状态类只关注自身状态下的行为。
- 遵循开闭原则 (OCP): 添加新状态只需添加新的
ConcreteState类,通常只需修改其相关的状态转换点(即调用setState()的位置)以及已有的前驱状态类(这些前驱状态可能需要转换到新状态)。 - 显式状态转换: 转换逻辑集中在状态类中,更易于理解和管理。
- 状态特定数据: 如果某个状态需要存储特定的临时数据(如等待超时的时间戳),可以放在该具体状态类中。
- 缺点:
- 类数量增多: 状态数量较多时,会产生大量的小类,增加系统复杂性。
- 状态转换理解成本: 状态转换规则分散在各个具体状态类的方法中,需要通读所有状态类才能完全掌握整个状态机的行为(相对
if-else集中在一处更难一眼看清所有转换路径)。
2. 状态机实现方式
对于非常复杂的状态机(特别是有大量状态和事件时),可以使用专门的状态机(FSM)库或框架(如 Spring StateMachine、SquirrelFJ)。这些框架通常基于状态模式或类似思想,但提供了更强大的功能:
-
定义状态和事件的枚举。
-
声明式(如注解或DSL)定义转换规则。
-
提供状态机实例管理、持久化、监控等。
-
优点:
- 管理复杂度: 专门为复杂状态机设计。
- 声明式配置: 转换规则集中配置,更清晰。
- 高级功能: 子状态、历史状态、并行状态、监控等。
-
缺点:
- 学习曲线: 需要学习框架。
- 过度设计: 简单状态机没必要使用。
六、最佳实践
- 谁负责状态转换?:
- 推荐: 让具体状态对象负责转换(如在上面的
UnpaidState.pay()中调用context.setState(new PaidState()))。这保持了状态转换逻辑与具体状态的绑定,符合职责分配,是标准模式的核心。 - 可选(但较少用): 让
Context在接收到请求并委托后,根据当前状态和执行结果来决定状态转换(通过if-else)。这通常是一种反模式,因为它容易把Context变成“上帝类”,又回到了使用分支语句的老路。
- 推荐: 让具体状态对象负责转换(如在上面的
- 管理上下文依赖:
- 状态对象通常需要访问上下文的数据(如订单ID、库存信息)才能执行操作。
- 解决方法是在状态方法的接口中传递
Context对象作为参数(如pay(context))(更常用,拉取)。 - 也可以让状态对象持有对上下文的引用(但要注意状态对象可复用性问题,可能状态对象不能被同一个
Context的多个实例共享)(较少用,潜在耦合)。
- 共享状态对象?: 如果状态类是无状态的(它们不包含任何字段,只使用传入的
Context数据),则可以被所有上下文实例共享(如static final常量实例),减少对象创建开销。如果状态需要持有自身特定的信息(如历史记录、计数器),则不能共享。 - 与策略模式区分:
- 状态模式: 焦点在状态。状态定义了行为及其如何导致状态的改变(转换)。行为随着内部状态自动改变,客户端通常感知不到状态的存在。
- 策略模式: 焦点在算法。客户端主动选择并使用一种策略来完成特定任务。策略通常不会改变上下文的状态,也不关心状态转换。策略之间是独立的。
七、在开发中的演变和应用
状态模式的思想在复杂系统和框架中广泛应用:
- 工作流引擎 (Workflow Engine): 业务流程管理 (BPM) 引擎的核心就是状态机。流程实例(
Context)的状态(如Active,Suspended,Completed)驱动着节点推进、任务分配和权限控制。状态模式是实现节点状态流转的底层机制之一。 - 游戏开发: 游戏角色(如 NPC, BOSS)、游戏实体(如门、机关)通常有多种状态(
Idle,Walking,Attacking,Dead)。每个状态控制着角色的行为、动画、碰撞检测等。状态模式是游戏 AI 和行为管理的常用手段。 - 网络协议栈: TCP连接的生命周期包含多个状态(
LISTEN,SYN_SENT,SYN_RECEIVED,ESTABLISHED,FIN_WAIT_1,FIN_WAIT_2,CLOSING,TIME_WAIT,CLOSED)。协议栈实现中,每个状态对象处理在该状态下接收到的数据包,并根据协议规则进行状态转换。 - UI 控件状态管理: 按钮等UI控件有
Normal,Hovered,Pressed,Disabled等状态。状态模式可用于管理控件在不同状态下的外观渲染和行为响应。
八、真实开发案例(Java语言内部、知名开源框架、工具)
java.util.Iterator(hasNext(),next(),remove()):- 虽然Iterator不是标准的State模式,但其底层迭代器的实现(如
ArrayList.Itr,HashMap.EntryIterator)需要根据集合内部状态(游标位置、是否有修改)来调整行为(如next()是否抛出ConcurrentModificationException)。可以说隐含了状态转换的思想。
- 虽然Iterator不是标准的State模式,但其底层迭代器的实现(如
- JDK 中的
javax.faces.lifecycle.Lifecycle(JSF):- 在Java Server Faces (JSF) 框架中,请求处理生命周期 (
Lifecycle) 的各个阶段 (RESTORE_VIEW,APPLY_REQUEST_VALUES,PROCESS_VALIDATIONS,UPDATE_MODEL_VALUES,INVOKE_APPLICATION,RENDER_RESPONSE) 可以被看作不同的状态。Lifecycle对象委托给各个阶段处理器(类似于状态对象)来执行阶段任务,并驱动生命周期进入下一个阶段(状态转换)。虽然不是严格的每个状态一个类,但思想一致。
- 在Java Server Faces (JSF) 框架中,请求处理生命周期 (
- Spring StateMachine (SSM):
- 这是Spring生态对复杂状态机支持的官方框架,其核心设计理念就基于状态模式。
- 开发者定义状态 (
State) 和事件 (Event)。 - 配置状态转换规则 (
Transition),由事件触发(sourceState -> event -> targetState)。 - 可以绑定动作(Action)到状态进入/退出(
onEntry/onExit)或转换(onTransition)时执行。 - 提供了丰富的功能:状态机实例管理(跨请求、集群)、状态机监听器、持久化(Repository)、区域(Region)用于管理子状态。
- 是处理复杂业务流程(如订单、工单、审批)的理想选择。
- JavaEE/CDI 会话 Bean 生命周期:
- 有状态会话Bean (SFSB) 的生命周期状态(
Does not exist,Passivated,Ready)管理也利用了状态模式的思想。容器根据调用、超时或钝化/激活事件改变Bean的状态,并触发相应的容器回调(@PostActivate,@PrePassivate),类似于状态类的入口/出口动作。
- 有状态会话Bean (SFSB) 的生命周期状态(
- Android Activity 生命周期:
- Android的
Activity有明确的生命周期状态(Created,Started,Resumed,Paused,Stopped,Destroyed)。开发者需要重写相应状态的回调方法(如onCreate(),onStart(),onResume())来执行状态相关的逻辑。这非常符合状态模式的思想:状态改变触发相应行为。
- Android的
九、总结
| 方面 | 总结 |
|---|---|
| 模式类型 | 行为型设计模式 |
| 核心意图 | 允许对象在内部状态改变时改变其行为,使其行为看起来像改变了类。消除与状态相关的条件分支。 |
| 关键角色 | 上下文(Context), 状态接口(State), 具体状态(ConcreteState) |
| 核心机制 | 1. 状态封装: 行为封装在独立状态类中。 2. 委托: 上下文将请求委托给当前状态对象。 3. 状态驱动转换: 具体状态对象根据逻辑调用 context.setState() 显式改变状态。 |
| 主要优点 | 1. 消除复杂条件分支,代码结构清晰。 2. 遵守 SRP 和 OCP,状态和行为易扩展。 3. 状态转换逻辑显式化、集中化。 4. 状态相关行为和数据局部化。 |
| 主要缺点 | 1. 类数量增加(状态多时)。 2. 状态转换逻辑可能分散(在各状态类中)。 3. 上下文方法需暴露设置状态的 setState(有时暴露过多)。 |
| 适用场景 | 对象行为高度依赖于状态,且状态转换复杂;存在大量状态判断分支;需要清晰管理状态流转。 |
| 最佳实践 | 状态对象负责转换;传递 Context 参数访问数据;可共享无状态状态对象;区分策略模式。 |
| 现代应用/思想 | 复杂工作流引擎的核心机制,游戏实体AI,协议栈实现,UI状态管理的理论基础。 |
| 真实案例 | Spring StateMachine (SSM) (最佳实践框架),JSF生命周期 (理念),Android生命周期 (理念),TCP状态 (理论基础)。 |
状态模式是管理复杂状态流转、消除“状态污染”代码的不二法门。它将状态提升为一等公民,使行为与状态绑定关系清晰可见、易于维护。虽然在状态数量庞大时需警惕类爆炸,但其带来的清晰度、扩展性和对核心设计原则的遵循使其成为处理状态驱动行为的首选模式。无论是构建灵活的工作流引擎、响应式的游戏 AI,还是管理复杂的 UI 状态,状态模式都为架构师提供了强大且优雅的解决方案。掌握状态模式,是驾驭对象行为复杂性的关键技能。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120770

浙公网安备 33010602011771号