状态模式:状态驱动的行为变化
状态模式:状态驱动的行为变化
引言
对象的行为往往依赖于其内部状态。当状态改变时,对象可以自动切换其行为模式。状态模式(State Pattern) 就是一种面向对象的设计模式,它允许一个对象在其内部状态改变时改变它的行为,看起来就像是改变了其所属的类一样。本教程将带你理解什么是状态模式,为什么需要它,以及如何在实际项目中正确应用。
1. 问题引出:多分支条件语句的困境
想象你正在开发一款电子设备控制系统,例如文档编辑器或自动售货机。这些系统有一个共同特点:系统行为随当前状态而变化。以简单的自动售货机为例,它可能有四种状态:无硬币状态、有硬币状态、售出商品状态、商品售罄状态。每个状态下,对用户的投币、退币、点击购买等操作的响应都不同。
public class GumballMachine {
final static int NO_COIN = 0;
final static int HAS_COIN = 1;
final static int SOLD = 2;
final static int SOLD_OUT = 3;
int state = SOLD_OUT;
int count = 0;
public void insertCoin() {
if (state == NO_COIN) {
System.out.println("硬币已投入");
state = HAS_COIN;
} else if (state == HAS_COIN) {
System.out.println("你已投入硬币,请勿重复投币");
} else if (state == SOLD) {
System.out.println("请稍后,正在出货");
} else if (state == SOLD_OUT) {
System.out.println("商品已售罄,无法投币");
}
}
// 其他方法类似...
}
这种实现的痛点很明显:
- 条件分支爆炸:每增加一个新状态,所有方法都需要修改
if-else分支。 - 可读性差:逻辑分散在大量条件判断中,难以维护。
- 违反开闭原则:对扩展开放,对修改封闭?实际上我们对修改是开放的。
- 状态转换逻辑混乱:状态码容易误用,转换条件散落在各处。
状态模式正是为了解决这类“由状态驱动行为”的系统设计问题。
2. 状态模式的核心思想
将每种状态封装成一个独立的类,并将与状态相关的行为委托给该对象。上下文(Context)只需维护一个表示当前状态的状态对象,所有请求都转发给该状态对象。状态对象可以自行决定是否切换到另一个状态。
官方定义:
状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
3. 模式结构
状态模式包含三个主要角色:
- Context(上下文):持有当前状态对象的引用,并定义客户端关心的接口。它将所有状态相关的操作委托给当前状态对象处理。
- State(状态接口):定义封装了与 Context 的一个特定状态相关的行为的接口。
- ConcreteState(具体状态类):实现 State 接口,提供特定状态下的行为实现。如果操作使状态发生变化,具体状态类会设置 Context 的新状态对象。
(图示:Context 持有 State 引用,多个 ConcreteState 实现同一接口,Context 将行为委托给 State)
4. 代码实现:重构自动售货机
我们使用状态模式重新设计售货机。
步骤1:定义状态接口
public interface State {
void insertCoin();
void ejectCoin();
void turnCrank();
void dispense();
}
步骤2:实现具体的状态类
public class NoCoinState implements State {
GumballMachine machine;
public NoCoinState(GumballMachine machine) {
this.machine = machine;
}
public void insertCoin() {
System.out.println("硬币已投入");
machine.setState(machine.getHasCoinState());
}
public void ejectCoin() { System.out.println("没有硬币可以退回"); }
public void turnCrank() { System.out.println("请先投币"); }
public void dispense() { System.out.println("需要先投币"); }
}
public class HasCoinState implements State {
GumballMachine machine;
public HasCoinState(GumballMachine machine) { this.machine = machine; }
public void insertCoin() { System.out.println("你已投入硬币,请勿重复投币"); }
public void ejectCoin() {
System.out.println("硬币已退回");
machine.setState(machine.getNoCoinState());
}
public void turnCrank() {
System.out.println("转动曲柄,商品即将出货...");
machine.setState(machine.getSoldState());
}
public void dispense() { System.out.println("无法直接取货,请先转动曲柄"); }
}
public class SoldState implements State {
GumballMachine machine;
public SoldState(GumballMachine machine) { this.machine = machine; }
public void insertCoin() { System.out.println("请稍后,正在出货"); }
public void ejectCoin() { System.out.println("你已经转动了曲柄,无法退币"); }
public void turnCrank() { System.out.println("转动两次也没用"); }
public void dispense() {
machine.releaseBall();
if (machine.getCount() > 0) {
machine.setState(machine.getNoCoinState());
} else {
System.out.println("哦,商品卖完了!");
machine.setState(machine.getSoldOutState());
}
}
}
public class SoldOutState implements State {
GumballMachine machine;
public SoldOutState(GumballMachine machine) { this.machine = machine; }
public void insertCoin() { System.out.println("商品已售罄,无法投币"); }
public void ejectCoin() { System.out.println("没有投入硬币,无法退币"); }
public void turnCrank() { System.out.println("已售罄,转动曲柄无效"); }
public void dispense() { System.out.println("没有商品可以发放"); }
}
步骤3:改造 Context(自动售货机)
public class GumballMachine {
State noCoinState;
State hasCoinState;
State soldState;
State soldOutState;
State currentState;
int count = 0;
public GumballMachine(int initialCount) {
noCoinState = new NoCoinState(this);
hasCoinState = new HasCoinState(this);
soldState = new SoldState(this);
soldOutState = new SoldOutState(this);
this.count = initialCount;
if (initialCount > 0) {
currentState = noCoinState;
} else {
currentState = soldOutState;
}
}
public void insertCoin() { currentState.insertCoin(); }
public void ejectCoin() { currentState.ejectCoin(); }
public void turnCrank() { currentState.turnCrank(); currentState.dispense(); } // 注意这里每次转动后调用 dispense
void setState(State state) { this.currentState = state; }
void releaseBall() {
System.out.println("一颗糖果滚出...");
if (count > 0) count--;
}
// getters...
}
现在,售货机的行为完全由当前状态对象决定,新增状态只需一个新类,无需修改原有代码。
5. 状态模式 vs. 策略模式
两者类图非常相似,但意图不同:
| 特性 | 状态模式 | 策略模式 |
|---|---|---|
| 目的 | 根据内部状态改变行为,状态通常由自身或上下文驱动变化。 | 在可互换的算法族中选择一个,通常由客户端指定。 |
| 状态/策略认知 | 具体状态之间可能互相知晓并触发转换。 | 策略之间彼此独立,互不了解。 |
| 转换控制 | Context 或具体状态类都可以主动改变状态。 | 客户端决定使用哪种策略,运行时不自动切换。 |
| 典型例子 | TCP 连接状态(监听、已建立、关闭);订单状态流转。 | 排序算法选择、支付方式选择。 |
简单说:状态模式是“受状态驱动的策略模式”,状态会自行转移。
6. 优缺点分析
优点
- 单一职责:将与特定状态相关的代码封装在独立类中。
- 开闭原则:无需修改现有状态或 Context,就能引入新状态。
- 消除庞大条件分支:让代码清晰无比。
- 状态转换显式化:转换逻辑集中在状态类内部或上下文,易于追踪。
缺点
- 类数量增加:每个状态都需要一个具体类,对于状态较少的系统可能显得小题大做。
- 状态类可能了解上下文细节:如果状态类需要操作 Context 的复杂数据,会造成耦合。
- 增加系统复杂性:简单的状态机用状态模式反而繁琐。
7. 实际应用场景
- 工作流引擎:请假审批流程(草稿→提交→审核中→通过/驳回)。
- 游戏开发:角色状态(站立、行走、攻击、死亡),每种状态响应不同按键输入。
- 网络连接:TCP 连接状态(CLOSED、LISTEN、SYN-SENT、ESTABLISHED 等),状态不同对数据的处理方式不同。
- 订单系统:待支付→已支付→已发货→已完成,每个状态允许的操作不同。
- 文档编辑器:只读模式、编辑模式、预览模式之间的切换。
8. 进阶:有状态的状态模式(状态拥有自己的状态)
有时状态类本身也会持有一些数据,比如售货机“有硬币状态”可能还要记录硬币面额。这样可以把更多行为内聚到状态对象中,使 Context 更简洁。但要注意生命周期管理,避免内存泄漏。
9. 总结
状态模式是处理“对象行为随状态改变”问题的利器。它通过将状态定义成独立的类,使复杂的条件判断逻辑变为多态调用,从而实现高内聚、低耦合的设计。当你面对一个充满 if-else 或 switch 且状态会频繁切换的类时,不妨考虑引入状态模式。但也要权衡类的数量增加带来的复杂度,对于简单的状态机,使用查表法或枚举或许更合适。
选择工具的关键在于评估系统未来状态变化的可能性与复杂性——如果状态可能增长,且行为复杂,状态模式能帮你轻松驾驭变化。