【设计模式】状态模式 (状态对象(Objects for States))
状态模式(State Pattern)详解
一、状态模式简介
状态模式(State Pattern) 是一种 行为型设计模式(对象行为型模式),它允许一个对象在其内部状态改变时改变其行为。换句话说,对象看起来好像修改了它的类。
你可以这样理解:
“同一个对象,在不同状态下表现出不同的行为。”
这就像一个自动售货机:
- 当你没有投币时,它不响应“出货”按钮;
- 投币后,它才允许你选择商品;
- 选择商品后,它才会出货并找零。
这个机器的行为随着内部状态(如“未投币”、“已投币”、“已选择”)的变化而变化。
-
红绿灯
-
H2O的三种状态(未考虑临界点):
在软件系统中:
有些对象具有多种状态。
这些状态在某些情况下能够相互转换。
对象在不同的状态下将具有不同的行为。
复杂的条件判断语句来进行状态的判断和转换操作 --> 导致代码的可维护性和灵活性下降 --> 出现新的状态时,代码的扩展性很差,客户端代码也需要进行相应的修改,违背了开闭原则。
// 非常多的if else
class TestXYZ
{int behaviour;//Getter and Setter......public void HandleAll(){if (behaviour == 0){ //do something }else if (behaviour == 1){ //do something }else if (behaviour == 2){ //do something }else if (behaviour == 3){ //do something }... some more else if ...}
}
又名状态对象(Objects for States)
用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。
将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象状态可以灵活变化。
对于客户端而言,无须关心对象状态的转换以及对象所处的当前状态,无论对于何种状态的对象,客户端都可以一致处理。
状态模式包含以下3个角色:
Context(环境类)
State(抽象状态类)
ConcreteState(具体状态类)
二、解决的问题类型
状态模式主要用于解决以下问题:
- 对象的行为依赖于其状态,并且状态较多导致代码中出现大量
if-else
或switch-case
判断逻辑。 - 希望将每种状态的行为封装到独立的类中,提高可读性和可维护性。
- 需要在运行时动态切换对象的行为。
三、使用场景
场景 | 示例 |
---|---|
工作流/审批系统 | 订单状态:待支付 → 已支付 → 发货中 → 已完成 |
游戏角色控制 | 角色状态:站立、奔跑、跳跃、死亡等 |
用户登录状态 | 未登录、已登录、锁定、超时等 |
电梯控制系统 | 运行、停止、开门、关门等状态 |
在有些情况下,多个环境对象可能需要共享同一个状态。
如果希望在系统中实现多个环境对象共享一个或多个状态对象,那么需要将这些状态对象定义为环境类的静态成员对象。
对于客户端而言,无须关心状态类,可以为环境类设置默认的状态类,将状态的转换工作交给环境类(或具体状态类)来完成,具体的转换细节对于客户端而言是透明的。
可以通过环境类来实现状态转换,环境类作为一个状态管理器,统一实现各种状态之间的转换操作。
对象的行为依赖于它的状态(例如某些属性值),状态的改变将导致行为的变化
在代码中包含大量与对象状态有关的条件语句,这些条件语句的出现会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,并且导致客户类与类库之间的耦合增强。
四、核心概念
- Context(上下文):持有当前状态对象的引用,对外提供接口。
- State(状态接口):定义所有具体状态类必须实现的行为。
- ConcreteState(具体状态类):实现状态接口,封装特定状态下的行为。
五、实际代码案例(Java)
我们以一个简单的“订单”系统为例,演示状态模式的使用。
1. 定义状态接口
// 订单状态接口
interface OrderState {void pay(OrderContext context);void ship(OrderContext context);void deliver(OrderContext context);void cancel(OrderContext context);
}
2. 创建具体状态类
// 待支付状态
class PendingState implements OrderState {@Overridepublic void pay(OrderContext context) {System.out.println("订单已支付,准备发货...");context.setState(new ShippedState()); // 转换到已发货状态}@Overridepublic void ship(OrderContext context) {System.out.println("请先支付!");}@Overridepublic void deliver(OrderContext context) {System.out.println("请先支付并发货!");}@Overridepublic void cancel(OrderContext context) {System.out.println("订单已取消。");context.setState(new CancelledState());}
}// 已发货状态
class ShippedState implements OrderState {@Overridepublic void pay(OrderContext context) {System.out.println("订单已支付过,无需重复支付。");}@Overridepublic void ship(OrderContext context) {System.out.println("订单已经在运输途中。");}@Overridepublic void deliver(OrderContext context) {System.out.println("货物已送达!");context.setState(new DeliveredState());}@Overridepublic void cancel(OrderContext context) {System.out.println("发货后无法取消订单。");}
}// 已送达状态
class DeliveredState implements OrderState {@Overridepublic void pay(OrderContext context) {System.out.println("订单已完成,无需支付。");}@Overridepublic void ship(OrderContext context) {System.out.println("订单已送达,无需发货。");}@Overridepublic void deliver(OrderContext context) {System.out.println("订单已送达,请勿重复操作。");}@Overridepublic void cancel(OrderContext context) {System.out.println("订单已完成,无法取消。");}
}// 已取消状态
class CancelledState implements OrderState {@Overridepublic void pay(OrderContext context) {System.out.println("订单已取消,无法支付。");}@Overridepublic void ship(OrderContext context) {System.out.println("订单已取消,无法发货。");}@Overridepublic void deliver(OrderContext context) {System.out.println("订单已取消,无法送达。");}@Overridepublic void cancel(OrderContext context) {System.out.println("订单已取消。");}
}
3. 创建上下文类(订单)
// 订单上下文
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 ship() {state.ship(this);}public void deliver() {state.deliver(this);}public void cancel() {state.cancel(this);}
}
4. 客户端测试类
public class Client {public static void main(String[] args) {OrderContext order = new OrderContext();System.out.println("=== 模拟订单流程 ===");order.pay(); // 支付order.ship(); // 发货(此时无实际发货动作,仅提示)order.deliver(); // 送达order.cancel(); // 尝试取消System.out.println("\n=== 尝试取消未支付订单 ===");OrderContext order2 = new OrderContext();order2.cancel();}
}
输出结果:
=== 模拟订单流程 ===
订单已支付,准备发货...
订单已经在运输途中。
货物已送达!
订单已完成,无法取消。=== 尝试取消未支付订单 ===
订单已取消。
典型代码案例:
典型的抽象状态类代码
abstract class State
{//声明抽象业务方法,不同的具体状态类可以有不同的实现public abstract void Handle();
}
典型的环境类代码
class Context
{private State state; //维持一个对抽象状态对象的引用private int value; //其他属性值,该属性值的变化可能会导致对象状态发生变化//设置状态对象public void SetState(State state)
{this.state = state;}public void Request()
{//其他代码state.Handle(); //调用状态对象的业务方法//其他代码}
}
状态转换的实现
- 统一由环境类来负责状态之间的转换,环境类充当了状态管理器(State Manager)角色。
……
public void ChangeState()
{//判断属性值,根据属性值进行状态转换
if (value == 0)
{this.SetState(new ConcreteStateA());}else if (value == 1)
{this.SetState(new ConcreteStateB());}......}……
- 由具体状态类来负责状态之间的转换,可以在具体状态类的业务方法中判断环境类的某些属性值,再根据情况为环境类设置新的状态对象,实现状态转换。
……
public void ChangeState(Context ctx)
{//根据环境对象中的属性值进行状态转换
if (ctx.Value == 1)
{ctx.SetState(new ConcreteStateB());}else if (ctx.Value == 2)
{ctx.SetState(new ConcreteStateC());}......}……
其他案例:
- 某软件公司要为一银行开发一套信用卡业务系统,银行账户(Account)是该系统的核心类之一,通过分析,该软件公司开发人员发现在系统中账户存在3种状态,且在不同状态下账户存在不同的行为,具体说明如下:
(1) 如果账户中余额大于等于0,则账户的状态为正常状态(Normal State),此时用户既可以向该账户存款也可以从该账户取款;
(2) 如果账户中余额小于0,并且大于-2000,则账户的状态为透支状态(Overdraft State),此时用户既可以向该账户存款也可以从该账户取款,但需要按天计算利息;
(3) 如果账户中余额等于-2000,那么账户的状态为受限状态(Restricted State),此时用户只能向该账户存款,不能再从中取款,同时也将按天计算利息;
(4) 根据余额的不同,以上3种状态可发生相互转换。
现使用状态模式设计并实现银行账户状态的转换。
- 论坛用户等级
在某论坛系统中,用户可以发表留言,发表留言将增加积分;用户也可以回复留言,回复留言也将增加积分;用户还可以下载文件,下载文件将扣除积分。该系统用户分为三个等级,分别是新手、高手和专家,这三个等级对应三种不同的状态,这三种状态分别定义如下:
(1) 如果积分小于100分,则为新手状态,用户可以发表留言、回复留言,但是不能下载文件。如果积分大于等于1000分,则转换为专家状态;如果积分大于等于100分,则转换为高手状态。
(2) 如果积分大于等于100分但小于1000分,则为高手状态,用户可以发表留言、回复留言,还可以下载文件,而且用户在发表留言时可以获取双倍积分。如果积分小于100分,则转换为新手状态;如果积分大于等于1000分,则转换为专家状态;如果下载文件后积分小于0,则不能下载该文件。
(3) 如果积分大于等于1000分,则为专家状态,用户可以发表留言、回复留言和下载文件,用户除了在发表留言时可以获取双倍积分外,下载文件只扣除所需积分的一半。如果积分小于100分,则转换为新手状态;如果积分小于1000分,但大于等于100,则转换为高手状态;如果下载文件后积分小于0,则不能下载该文件。
- 某系统要求两个开关对象要么都处于开的状态,要么都处于关的状态,在使用时它们的状态必须保持一致,开关可以由开转换到关,也可以由关转换到开。
试使用状态模式来实现开关的设计。
- 现要开发一个屏幕放大镜工具,其具体功能描述如下:
用户单击“放大镜”按钮之后屏幕将放大一倍,再单击一次“放大镜”按钮屏幕再放大一倍,第三次单击该按钮后屏幕将还原到默认大小。
试使用状态模式来设计该屏幕放大镜工具。
六、优缺点分析
优点 | 描述 |
---|---|
✅ 消除复杂的条件判断语句 | 将 if-else 或 switch 分散到各个状态类中,结构清晰。允许状态转换逻辑与状态对象合成一体,而不是提供一个巨大的条件语句块,可以避免使用庞大的条件语句来将业务方法和状态转换代码交织在一起 |
✅ 符合开闭原则 | 新增状态只需添加新类,无需修改现有代码 |
✅ 职责分离 | 每个状态类只关注自己状态下的行为,职责明确 |
其他 | 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。封装了状态的转换规则,可以对状态转换代码进行集中管理,而不是分散在一个个业务方法中。 |
缺点 | 描述 |
---|---|
❌ 增加类的数量 | 每个状态对应一个类,可能导致类膨胀 |
❌ 状态转换逻辑分散 | 状态之间的流转由具体状态类控制,可能不够集中。对开闭原则的支持并不太好。增加新的状态类需要修改负责状态转换的源代码,否则无法转换到新增状态;而且修改某个状态类的行为也需要修改对应类的源代码 |
❌ 不适合状态较少的场景 | 如果状态只有两三种,使用状态模式反而显得过度设计。结构与实现都较为复杂,如果使用不当将导致程序结构和代码混乱,增加系统设计的难度 |
七、与策略模式对比(补充)
对比点 | 状态模式 | 策略模式 |
---|---|---|
目的 | 改变对象的行为以反映其状态变化 | 封装算法,使算法可互换 |
状态流转 | 状态之间可以相互转换 | 策略之间通常是独立的 |
上下文关系 | 上下文行为随状态变化而变 | 上下文使用策略完成某项任务 |
八、最终小结
状态模式是一种非常实用的设计模式,特别适用于那些具有多个状态、且每个状态下行为差异较大的对象。它通过将每种状态封装成独立的类,使程序结构更清晰、易于扩展和维护。
在开发订单系统、工作流引擎、游戏状态管理、UI 控件状态控制等项目中,状态模式能有效提升代码的可读性和可维护性。
📌 一句话总结:
状态模式就像一个“状态驱动的行为控制器”,让对象根据自己的“心情”(状态)做出不同的反应。
✅ 推荐使用场景:
- 对象有多个状态,且状态转换频繁;
- 使用大量
if-else
或switch-case
判断状态; - 需要后期扩展更多状态。
注意,以上部分内容由AI大模型生成,注意识别!