设计模式-状态模式
文章目录
一、概述
在软件开发中,经常会遇到这样的场景:一个对象的行为会随着其内部状态的改变而变化。例如,订单从"待支付"→"已支付"→"已发货"→"已完成"的流转过程中,每个状态下允许的操作都不同;电梯在"停止"“上行”“下行"三种状态下,对"开门”“关门”“上行”"下行"指令的响应也不同。如果使用大量的 if-else 或 switch-case 来管理状态切换,代码会变得臃肿、晦涩、难以维护:
// 反例:用 if-else 管理状态——代码会越来越难维护
public void handleOrder(String action) {
if ("PENDING".equals(state)) {
if ("pay".equals(action)) {
state = "PAID";
}
if ("cancel".equals(action)) {
state = "CANCELLED";
}
} else if ("PAID".equals(state)) {
if ("ship".equals(action)) {
state = "SHIPPED";
}
if ("refund".equals(action)) {
state = "REFUNDING";
}
} else if ("SHIPPED".equals(state)) {
if ("confirm".equals(action)) {
state = "COMPLETED";
}
}
// ... 随着状态增多,分支爆炸
}
状态模式(State Pattern)正是为了解决这个问题而诞生的——它允许对象在其内部状态改变时改变它的行为,使得对象看起来似乎修改了它的类。状态模式将与特定状态相关的行为封装到独立的状态类中,并通过状态对象内部自动完成切换,消除了臃肿的条件判断。
生活中的状态模式例子:
- 自动售货机:有"无币"“有币”“出货”"售罄"四种状态,在不同状态下投币、退币、出货的响应不同
- 电梯系统:在"停止"“上行”"下行"三种状态下,对开门、关门、目的地按钮的响应不同,状态自动切换
- 文档审批流程:文档从"草稿"→"已提交"→"审核中"→"已批准"/“已驳回”,每个状态下可执行的操作不同
- TCP 连接:连接在 CLOSED、LISTEN、SYN_SENT、ESTABLISHED 等状态间自动切换,状态决定了对网络事件的响应
核心:允许对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。将与特定状态相关的行为封装到独立的状态类中,让状态转换由状态对象内部自动完成。
1.1 结构与角色
状态模式包含以下角色:
- State(抽象状态):定义所有具体状态的公共接口,声明在特定状态下可执行的行为
- ConcreteState(具体状态):实现抽象状态接口,封装与该状态相关的行为,并在需要时切换上下文的状态
- Context(上下文):持有当前状态的引用,将客户端请求委托给当前状态对象处理,提供切换状态的方法供具体状态调用
- Client(客户端):创建上下文和初始状态对象,通过上下文接口触发状态行为
与策略模式的关键区别:状态模式的转换是自动的、由状态对象内部控制的,而策略模式的切换是由客户端主动选择的。
1.2 适用场景
- 对象的行为依赖于它的状态,且必须在运行时根据状态改变它的行为
- 代码中包含大量与对象状态相关的条件判断(
if-else/switch-case) - 状态数量较多且状态转换逻辑复杂,条件判断会导致代码臃肿
- 需要将状态转换逻辑和状态对应的行为分离,使其独立变化
二、实现方式
状态模式的核心实现思路是:将不同状态下的行为分别封装为独立的状态类,它们实现同一个状态接口,上下文通过组合持有当前状态的引用,将行为的执行委托给当前状态对象。状态的切换由状态对象内部触发,通过上下文提供的 setState 方法完成。
以"订单状态流转"为例,订单从"待支付"→"已支付"→"已发货"→"已完成",不同状态下可执行的操作不同:
2.1 基础实现
(1)抽象状态接口——订单状态
/**
* 抽象状态:订单状态
* 定义所有状态下共有的行为
*/
public interface OrderState {
/**
* 支付
*/
void pay(OrderContext context);
/**
* 发货
*/
void ship(OrderContext context);
/**
* 确认收货
*/
void confirm(OrderContext context);
/**
* 取消订单
*/
void cancel(OrderContext context);
/**
* 获取状态名称
*
* @return 状态名称
*/
String getStateName();
}
(2)上下文——订单上下文
/**
* 上下文:订单上下文
* 持有当前状态的引用,将行为委托给当前状态对象
* 提供 setState() 方法供具体状态类在内部切换状态
*/
public class OrderContext {
/** 当前状态 */
private OrderState currentState;
/** 订单号 */
private final String orderId;
public OrderContext(String orderId) {
this.orderId = orderId;
// 初始状态:待支付
this.currentState = new PendingState();
System.out.println("订单 " + orderId + " 已创建,当前状态:" + currentState.getStateName());
}
/**
* 切换状态(由具体状态类内部调用)
*
* @param newState 新状态
*/
public void setState(OrderState newState) {
System.out.println("订单 " + orderId + " 状态变更:" + this.currentState.getStateName()
+ " → " + newState.getStateName());
this.currentState = newState;
}
// --- 将行为委托给当前状态 ---
public void pay() {
currentState.pay(this);
}
public void ship() {
currentState.ship(this);
}
public void confirm() {
currentState.confirm(this);
}
public void cancel() {
currentState.cancel(this);
}
public String getCurrentStateName() {
return currentState.getStateName();
}
}
(3)具体状态——待支付状态
/**
* 具体状态:待支付
* 在该状态下可以支付或取消,不能发货或确认收货
*/
public class PendingState 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("【错误】订单尚未支付,无法确认收货");
}
@Override
public void cancel(OrderContext context) {
System.out.println("订单已取消");
// 切换到已取消状态
context.setState(new CancelledState());
}
@Override
public String getStateName() {
return "待支付";
}
}
(4)具体状态——已支付状态
/**
* 具体状态:已支付
* 在该状态下可以发货或退款,不能支付或确认收货
*/
public 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("【错误】订单尚未发货,无法确认收货");
}
@Override
public void cancel(OrderContext context) {
System.out.println("订单已退款");
// 切换到退款中状态
context.setState(new RefundingState());
}
@Override
public String getStateName() {
return "已支付";
}
}
(5)具体状态——已发货状态
/**
* 具体状态:已发货
* 在该状态下只能确认收货,不能支付、发货或取消
*/
public 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());
}
@Override
public void cancel(OrderContext context) {
System.out.println("【错误】订单已发货,无法取消,请联系客服");
}
@Override
public String getStateName() {
return "已发货";
}
}
(6)终态——已完成、已取消、退款中
/**
* 具体状态:已完成(终态)
*/
public 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("【错误】订单已完成,无需重复确认");
}
@Override
public void cancel(OrderContext context) {
System.out.println("【错误】订单已完成,无法取消");
}
@Override
public String getStateName() {
return "已完成";
}
}
/**
* 具体状态:已取消(终态)
*/
public class CancelledState 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("【错误】订单已取消,无法确认收货");
}
@Override
public void cancel(OrderContext context) {
System.out.println("【错误】订单已取消,请勿重复操作");
}
@Override
public String getStateName() {
return "已取消";
}
}
/**
* 具体状态:退款中(终态)
*/
public class RefundingState 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("【错误】订单退款中,无法确认收货");
}
@Override
public void cancel(OrderContext context) {
System.out.println("【错误】订单退款中,请勿重复操作");
}
@Override
public String getStateName() {
return "退款中";
}
}
(7)客户端调用
public class StatePatternDemo {
public static void main(String[] args) {
OrderContext order = new OrderContext("ORD-20240519-001");
System.out.println("\n--- 正常流程 ---");
order.pay(); // 待支付 → 已支付
order.ship(); // 已支付 → 已发货
order.confirm(); // 已发货 → 已完成
System.out.println("\n--- 异常操作测试 ---");
OrderContext order2 = new OrderContext("ORD-20240519-002");
order2.ship(); // 错误:尚未支付
order2.cancel(); // 待支付 → 已取消
order2.pay(); // 错误:已取消
}
}
输出结果:
订单 ORD-20240519-001 已创建,当前状态:待支付
--- 正常流程 ---
支付成功!
订单 ORD-20240519-001 状态变更:待支付 → 已支付
发货成功!
订单 ORD-20240519-001 状态变更:已支付 → 已发货
确认收货成功!订单已完成
订单 ORD-20240519-001 状态变更:已发货 → 已完成
--- 异常操作测试 ---
订单 ORD-20240519-002 已创建,当前状态:待支付
【错误】订单尚未支付,无法发货
订单已取消
订单 ORD-20240519-002 状态变更:待支付 → 已取消
【错误】订单已取消,无法支付
关键点:每个状态类只关心自己状态下的合法行为,状态切换由状态对象内部调用
context.setState()完成,客户端无需关心状态转换逻辑。新增状态时只需新增一个实现类,无需修改已有状态类。
2.2 状态模式 vs if-else 对比
以 6 种订单状态为例:
| 对比维度 | if-else 实现 | 状态模式 |
|---|---|---|
| 代码结构 | 一个类中集中所有状态逻辑 | 每个状态独立封装为一个类 |
| 新增状态 | 修改已有类,在所有分支方法中加 else-if | 新增一个状态类,无需修改已有代码 |
| 状态转换 | 直接修改状态字段,容易遗漏 | 状态对象内部通过 setState 切换,逻辑清晰 |
| 代码可读性 | 分支嵌套让逻辑难以理解 | 每个状态类职责单一,一目了然 |
| 符合开闭原则 | 否 | 是(对扩展开放,对修改关闭) |
| 状态数量 | 少时简单 | 每增加一个状态就增加一个类 |
三、状态模式 vs 策略模式
状态模式和策略模式在结构上非常相似——都是通过组合持有一个接口引用来委托行为。但它们的意图和使用方式有着本质区别:
3.1 核心区别
| 对比维度 | 策略模式 | 状态模式 |
|---|---|---|
| 意图 | 定义一系列可互换的算法,让客户端任选其一 | 根据内部状态自动切换行为 |
| 切换方式 | 客户端主动选择并设置策略 | 状态对象内部自动切换 |
| 策略/状态间关系 | 策略之间互不感知,平等替换 | 状态之间存在明确的流转关系 |
| 上下文依赖 | 上下文不关心具体策略,只委托调用 | 上下文被状态对象反向引用以完成切换 |
| 生命周期 | 策略通常无状态(或有注入的配置) | 状态对象持有上下文引用,驱动转换 |
| 典型场景 | 支付方式选择、排序策略、出行方式 | 订单流转、电梯状态、审批流程 |
3.2 代码对比
策略模式——客户端主动选择:
// 策略模式:客户端决定使用哪种支付策略
PaymentContext ctx = new PaymentContext();
ctx.setStrategy(new WechatPay()); // 客户端主动选择微信支付
ctx.executePay(100);
ctx.setStrategy(new AliPay()); // 客户端主动切换到支付宝
ctx.executePay(200);
状态模式——状态内部自动切换:
// 状态模式:状态由对象内部自动切换,客户端只触发行为
OrderContext order = new OrderContext("ORD-001");
order.pay(); // 状态内部:待支付 → 已支付(自动)
order.ship(); // 状态内部:已支付 → 已发货(自动)
order.confirm();// 状态内部:已发货 → 已完成(自动)
3.3 选型指南
| 判断依据 | 选择策略模式 | 选择状态模式 |
|---|---|---|
| 切换由谁决定? | 客户端主动选择 | 状态对象内部自动决定 |
| 存在状态流转? | 策略间无流转关系 | 状态间有明确的转换路径 |
| 可任意替换? | 是,策略可任意组合 | 否,受状态机规则约束 |
| 代码中大量 if-else? | 用于选择算法的条件分支 | 用于判断当前状态的条件分支 |
四、总结
状态模式的核心思想是允许对象在其内部状态改变时改变它的行为,将与特定状态相关的行为封装到独立的状态类中,让状态转换由状态对象内部自动完成。
优点:
- 消除条件判断:将大量
if-else/switch-case分解到各个状态类中,每个类职责单一 - 符合开闭原则:新增状态只需新增一个状态类,无需修改已有状态类和上下文
- 状态转换显式化:状态转换逻辑集中在状态类中,流转路径一目了然
- 提高可维护性:每个状态的逻辑独立封装,修改某个状态不影响其他状态
- 行为随状态变化:同一操作在不同状态下有不同的响应,符合现实业务逻辑
缺点:
- 增加类的数量:每个状态都需要一个独立的类,状态越多类越多
- 状态类间的耦合:具体状态类需要知道下一个状态是哪个,状态间存在一定的耦合
- 不适合状态少且固定的场景:如果只有 2-3 个简单状态,使用状态模式反而增加复杂度
- 上下文被状态类反向引用:状态类持有上下文的引用,存在循环依赖
适用场景:
- 对象的行为依赖于它的状态,且必须在运行时根据状态改变它的行为
- 操作中包含大量与对象状态相关的条件判断(
if-else/switch-case) - 状态数量较多且转换逻辑复杂,需要将状态转换显式化
- 需要通过状态机来管理对象的生命周期(如订单、审批、工作流)
状态模式 vs 策略模式:两者结构相似但意图不同——策略模式让客户端主动选择算法,策略间平等可互换;状态模式让对象根据内部状态自动切换行为,状态间有明确的流转规则。简单记忆:策略模式是"我想用什么就用什么",状态模式是"当前状态决定我能做什么"。
参考博客:
状态模式 | 菜鸟教程:https://www.runoob.com/design-pattern/state-pattern.html
更多推荐
所有评论(0)