Java 设计模式心法之第23篇 - 状态 (State) - 让对象的行为随状态优雅切换
在许多软件系统中,一个对象的行为往往会随着其内部状态的不同而发生显著的变化。例如,一个订单可以处于“待支付”、“已支付”、“已发货”、“已完成”、“已取消”等不同状态,其可执行的操作(如支付、发货、取消、申请退款)在不同状态下是不同的;或者一个网络连接可以处于“未连接”、“连接中”、“已连接”、“已断开”状态,其收发数据的行为也随之改变。如果直接在对象的方法中使用大量的 if-else
或 switch
语句来判断当前状态并执行相应的行为,会导致方法极其冗长、复杂、难以维护,并且每次增加新状态或修改状态行为都需要改动这个庞大的条件判断结构。本文将带你深入理解行为型模式中的“状态驱动者”——状态模式。我们将揭示它如何将每种状态下的行为封装到独立的、实现了共同接口的状态类中,并将对象的当前状态作为一个成员变量(通常是状态对象引用)。当对象的行为被调用时,它将该调用委托给当前的状态对象来处理。更妙的是,状态的转换逻辑也可以封装在状态类内部,使得对象在特定条件下能够平滑地切换到下一个状态,仿佛对象“改变了它的类”一样。
一、问题的提出:当“行为多变”遭遇“条件判断地狱”
想象一下你正在开发一个自动售货机 (Vending Machine) 的控制程序:
售货机有几种主要状态:
- 无币状态 (NoCoinState): 没有投入硬币。
- 有币状态 (HasCoinState): 已经投入硬币,等待选择商品。
- 售罄状态 (SoldOutState): 商品已卖完。
- 售出状态 (SoldState): 正在出货。
售货机可以执行几种操作:
- 投币 (insertCoin())
- 退币 (ejectCoin())
- 选择商品 (selectItem())
- 出货 (dispense())
关键在于,同一个操作在不同状态下会产生完全不同的行为:
- 投币:
- 在“无币状态”下,投币成功,状态变为“有币状态”。
- 在“有币状态”下,提示已投币,不接受更多硬币。
- 在“售罄状态”下,退回硬币。
- 在“售出状态”下,提示请稍等,不接受硬币。
- 退币:
- 在“有币状态”下,退回硬币,状态变回“无币状态”。
- 在其他状态下,无法退币。
- 选择商品:
- 在“有币状态”下,选择有效商品,状态变为“售出状态”,并触发“出货”动作。
- 在其他状态下,选择无效。
- 出货:
- 在“售出状态”下,发放商品,检查库存,如果还有货则变为“无币状态”,如果没货了则变为“售罄状态”。
- 在其他状态下,不能出货。
如果我们把所有这些逻辑都写在 VendingMachine
类的方法里,使用 if-else
或 switch
来判断当前状态 (currentState
):
// 糟糕的设计:状态判断逻辑充斥在每个方法中
enum MachineState { NO_COIN, HAS_COIN, SOLD_OUT, SOLD }
class VendingMachineBad {MachineState currentState = MachineState.SOLD_OUT; // 初始状态int itemCount = 0; // 商品数量public VendingMachineBad(int initialCount) {this.itemCount = initialCount;if (itemCount > 0) {currentState = MachineState.NO_COIN;}}public void insertCoin() {System.out.print("投币 -> ");switch (currentState) {case NO_COIN:System.out.println("投币成功!");currentState = MachineState.HAS_COIN;break;case HAS_COIN:System.out.println("已经有硬币了,不能再投!");break;case SOLD_OUT:System.out.println("商品售罄,退回硬币。");break;case SOLD:System.out.println("正在出货,请稍等,不能投币。");break;}}public void ejectCoin() {System.out.print("退币 -> ");switch (currentState) {case HAS_COIN:System.out.println("退回硬币。");currentState = MachineState.NO_COIN;break;// 在 NO_COIN, SOLD_OUT, SOLD 状态下退币无效default: System.out.println("无法退币。");}}public void selectItem() {System.out.print("选择商品 -> ");switch (currentState) {case HAS_COIN:System.out.println("选择了商品...");currentState = MachineState.SOLD;dispense(); // 触发 dispensebreak;// 在 NO_COIN, SOLD_OUT, SOLD 状态下选择无效default: System.out.println("选择无效。");}}private void dispense() { // dispense 通常是内部方法System.out.print("出货 -> ");switch (currentState) {case SOLD:System.out.println("发放商品!");itemCount--;if (itemCount > 0) {currentState = MachineState.NO_COIN;System.out.println("...现在回到无币状态。");} else {currentState = MachineState.SOLD_OUT;System.out.println("...商品已售罄!");}break;// 在其他状态下不能出货default: System.out.println("无法出货。");}}// ... 可能还有其他方法,都需要类似的 switch 判断 ...
}
这样的代码简直是一场灾难:
- 难以阅读和维护: 每个方法都充斥着庞大的
switch
或if-else
结构,逻辑分散且难以理解。 - 违反开闭原则 (OCP): 如果要增加一个新的状态(比如“维护中状态”),或者修改某个状态下的行为,必须回来修改所有包含状态判断的方法。
- 状态转换逻辑分散: 状态之间的转换逻辑(如
currentState = MachineState.HAS_COIN
)散布在各个方法的不同分支中,难以追踪和管理。 - 容易出错: 遗漏某个状态的处理或者写错某个分支的逻辑是很常见的错误。
我们需要一种方法,能够将特定状态下的行为以及状态转换的逻辑封装起来,让 VendingMachine
类本身变得简洁,并且易于扩展新的状态。
二、状态对象化:状态模式的核心定义与意图
状态模式 (State Pattern) 提供了一种优雅的解决方案。它允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
GoF 的经典意图描述是:“允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。”
其核心思想在于:
- 定义状态接口/抽象类 (State Interface/Abstract Class): 定义一个接口或抽象类,声明了所有可能的状态都需要响应的操作方法(例如,对应售货机的
insertCoin()
,ejectCoin()
,selectItem()
,dispense()
等)。 - 创建具体状态类 (Concrete State Classes): 为系统中的每一种状态创建一个实现了 State 接口的具体状态类(如
NoCoinState
,HasCoinState
,SoldOutState
,SoldState
)。- 封装状态下的行为: 每个具体状态类只负责实现在该状态下,对于各个操作方法应该执行的具体行为。例如,
NoCoinState
的insertCoin()
方法会执行接收硬币并转换到HasCoinState
的逻辑,而它的ejectCoin()
方法则什么也不做或提示错误。 - 封装状态转换逻辑: 状态之间的转换通常也由状态类自身来负责。例如,在
NoCoinState
的insertCoin()
方法中,完成接收硬币的操作后,它会调用 Context 对象(售货机)的方法将 Context 的当前状态切换为HasCoinState
的实例。
- 封装状态下的行为: 每个具体状态类只负责实现在该状态下,对于各个操作方法应该执行的具体行为。例如,
- 上下文类持有状态引用 (Context Holds State Reference): 创建一个上下文 (Context) 类(即我们的
VendingMachine
),它内部持有一个指向**当前状态对象 (State 接口类型)**的引用。 - 上下文委托状态执行 (Context Delegates to State): 当 Context 的某个操作方法被调用时(如
vendingMachine.insertCoin()
),它不再自己进行if-else
判断,而是直接将该调用委托给其当前持有的那个状态对象的相应方法(currentState.insertCoin(this)
,通常会将 Context 自身this
传递给状态对象,以便状态对象能够调用 Context 的方法来改变 Context 的状态或访问其数据)。
核心角色:
- Context (上下文):
- 维护一个 ConcreteState 子类的实例,这个实例定义了对象的当前状态。
- 将所有与状态相关的请求委托给当前的状态对象处理。
- 可以提供一个 Setter 方法让 State 对象可以改变 Context 的当前状态。
- State (状态接口/抽象类): 定义一个接口,用于封装与 Context 的一个特定状态相关的行为。
- ConcreteState (具体状态类): 每一个子类实现一个与 Context 的一种状态相关的行为。负责处理来自 Context 的请求,并可以决定在适当的时候改变 Context 的状态(切换到下一个状态)。
关键:将每种状态的行为和转换逻辑封装到独立的状态类中,Context 将请求委托给当前状态对象处理,状态对象负责行为实现和状态切换。
三、行为随心转:状态模式的适用场景
状态模式非常适用于以下情况:
- 一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为: 这是状态模式最核心的应用场景。
- 一个操作中含有庞大的多分支条件语句,且这些分支依赖于该对象的状态: 状态模式将每一个分支封装到一个独立的状态类中,消除了
if-else
或switch-case
。 - 状态转换逻辑复杂且分散: 状态模式可以将状态转换的逻辑集中到具体状态类中,使得状态转换更加清晰和易于管理。
- 希望代码符合开闭原则: 添加新的状态只需要增加一个新的 ConcreteState 类,通常只需要修改触发状态转换的那个状态类的代码即可,对 Context 和其他状态类影响很小。
Java 中的应用思考:
- TCP 连接状态: 一个 TCP 连接可以有
CLOSED
,LISTEN
,SYN_SENT
,SYN_RCVD
,ESTABLISHED
,CLOSE_WAIT
,LAST_ACK
,FIN_WAIT_1
,FIN_WAIT_2
,TIME_WAIT
等多种状态,其对接收数据、发送数据、关闭连接等操作的响应在不同状态下完全不同。状态模式是模拟这种行为的理想选择。 - 工作流引擎: 流程中的一个任务实例可以有“待处理”、“处理中”、“已完成”、“已驳回”、“已挂起”等状态,不同状态下允许的操作和下一步流转不同。
- UI 控件的状态: 一个按钮可以有“正常”、“悬停”、“按下”、“禁用”等状态,其外观和响应行为会随状态变化。
四、状态驱动的实现:状态模式的 Java 实践
我们用自动售货机的例子来实现状态模式。
1. 定义状态接口 (State):
/*** 状态接口:定义了在每种状态下可以执行的操作*/
interface State {void insertCoin(VendingMachine machine);void ejectCoin(VendingMachine machine);void selectItem(VendingMachine machine);void dispense(VendingMachine machine); // 通常由 selectItem 触发,或者作为内部状态转换的一部分String getStateName(); // (可选) 用于调试或显示状态名称
}
2. 创建上下文类 (Context):
/*** 上下文类:自动售货机*/
class VendingMachine {// 持有所有可能的状态对象 (通常设为 final,可以共享)final State noCoinState;final State hasCoinState;final State soldState;final State soldOutState;// 当前状态State currentState;// 商品数量int count = 0;public VendingMachine(int initialCount) {// 创建所有状态对象,并将自身(this)传递给它们noCoinState = new NoCoinState(this);hasCoinState = new HasCoinState(this);soldState = new SoldState(this);soldOutState = new SoldOutState(this);this.count = initialCount;if (count > 0) {currentState = noCoinState; // 设置初始状态System.out.println("售货机初始化,库存: " + count + ", 初始状态: " + currentState.getStateName());} else {currentState = soldOutState;System.out.println("售货机初始化,库存为 0, 初始状态: " + currentState.getStateName());}}// --- 将操作委托给当前状态对象 ---public void insertCoin() {System.out.print("操作:投币 -> ");currentState.insertCoin(this);}public void ejectCoin() {System.out.print("操作:退币 -> ");currentState.ejectCoin(this);}public void selectItem() {System.out.print("操作:选择商品 -> ");// 选择商品和出货通常是连续的原子操作,可以在 selectItem 里直接调用 dispense// 或者让 selectItem 改变状态到 soldState,然后立刻调用 dispensecurrentState.selectItem(this);// dispense(); // 或者在这里调用,取决于状态实现}// ( dispense 通常是内部状态转换的一部分,不由客户端直接调用,但状态对象需要能调用它 )void dispenseItem() {currentState.dispense(this);}// --- 提供给状态对象用于改变状态和访问数据的方法 ---void setState(State state) {System.out.println("状态转换: 从 " + this.currentState.getStateName() + " -> " + state.getStateName());this.currentState = state;}int getItemCount() { return count; }void releaseItem() {if (count > 0) {System.out.println("...发放一件商品...");count--;}}// --- Getter for states (状态对象需要能访问其他状态实例来进行切换) ---State getNoCoinState() { return noCoinState; }State getHasCoinState() { return hasCoinState; }State getSoldState() { return soldState; }State getSoldOutState() { return soldOutState; }@Override public String toString() { return "当前状态: " + currentState.getStateName() + ", 库存: " + count; }
}
3. 创建具体状态类 (Concrete State):
/*** 具体状态A:无币状态*/
class NoCoinState implements State {VendingMachine machine;public NoCoinState(VendingMachine machine) { this.machine = machine; }@Override public String getStateName() { return "无币状态"; }@Override public void insertCoin(VendingMachine machine) {System.out.println("投币成功!");machine.setState(machine.getHasCoinState()); // 状态转换到 HasCoin}@Override public void ejectCoin(VendingMachine machine) { System.out.println("您还没有投币,无法退币。"); }@Override public void selectItem(VendingMachine machine) { System.out.println("请先投币再选择商品。"); }@Override public void dispense(VendingMachine machine) { System.out.println("非法操作:请先投币并选择商品。"); }
}/*** 具体状态B:有币状态*/
class HasCoinState implements State {VendingMachine machine;public HasCoinState(VendingMachine machine) { this.machine = machine; }@Override public String getStateName() { return "有币状态"; }@Override public void insertCoin(VendingMachine machine) { System.out.println("您已经投过币了,请勿重复投币。"); }@Override public void ejectCoin(VendingMachine machine) {System.out.println("退回硬币。");machine.setState(machine.getNoCoinState()); // 状态转换回 NoCoin}@Override public void selectItem(VendingMachine machine) {System.out.println("选择了商品...");// 模拟随机中奖逻辑 (可以增加一个 WinnerState)// 这里简化处理,直接进入售出状态machine.setState(machine.getSoldState()); // 状态转换到 Soldmachine.dispenseItem(); // 立刻尝试出货 (或者由 SoldState 的 entry action 触发)}@Override public void dispense(VendingMachine machine) { System.out.println("非法操作:请先选择商品。"); }
}/*** 具体状态C:售出状态 (正在出货)*/
class SoldState implements State {VendingMachine machine;public SoldState(VendingMachine machine) { this.machine = machine; }@Override public String getStateName() { return "售出状态(出货中)"; }@Override public void insertCoin(VendingMachine machine) { System.out.println("正在出货,请稍等,不能投币。"); }@Override public void ejectCoin(VendingMachine machine) { System.out.println("正在出货,无法退币。"); }@Override public void selectItem(VendingMachine machine) { System.out.println("正在出货,请勿重复选择。"); }@Override public void dispense(VendingMachine machine) {machine.releaseItem(); // 发放商品(减少库存)if (machine.getItemCount() > 0) {machine.setState(machine.getNoCoinState()); // 如果还有货,回到无币状态} else {System.out.println("糟糕!商品已售罄!");machine.setState(machine.getSoldOutState()); // 如果没货了,进入售罄状态}}
}/*** 具体状态D:售罄状态*/
class SoldOutState implements State {VendingMachine machine;public SoldOutState(VendingMachine machine) { this.machine = machine; }@Override public String getStateName() { return "售罄状态"; }@Override public void insertCoin(VendingMachine machine) { System.out.println("对不起,商品已售罄,退回硬币。"); }@Override public void ejectCoin(VendingMachine machine) { System.out.println("您没有投币(且商品售罄),无法退币。"); }@Override public void selectItem(VendingMachine machine) { System.out.println("商品已售罄,选择无效。"); }@Override public void dispense(VendingMachine machine) { System.out.println("非法操作:商品已售罄。"); }// (可以增加一个 refill(int count) 方法来补充库存并转换回 NoCoinState)
}
4. 客户端使用:
public class StateClient {public static void main(String[] args) {// 1. 创建售货机 (Context),初始有 2 件商品VendingMachine machine = new VendingMachine(2);System.out.println(machine); // 初始状态System.out.println("\n--- 模拟操作 ---");machine.insertCoin(); // 投币 -> HasCoinSystem.out.println(machine);machine.selectItem(); // 选择商品 -> Sold -> (dispense) -> NoCoin (库存1)System.out.println(machine);System.out.println("\n--- 再次购买 ---");machine.insertCoin(); // 投币 -> HasCoinSystem.out.println(machine);machine.ejectCoin(); // 退币 -> NoCoinSystem.out.println(machine);machine.insertCoin(); // 再次投币 -> HasCoinSystem.out.println(machine);machine.selectItem(); // 选择商品 -> Sold -> (dispense) -> SoldOut (库存0)System.out.println(machine);System.out.println("\n--- 售罄后尝试操作 ---");machine.insertCoin(); // 尝试投币 -> SoldOut (退币)System.out.println(machine);machine.selectItem(); // 尝试选择 -> SoldOut (无效)System.out.println(machine);// 客户端代码非常简单,只管调用 VendingMachine 的方法,// 完全不关心内部状态的判断和转换逻辑,这些都由当前的状态对象负责了。}
}
代码解读:
State
接口定义了所有状态都需要响应的操作。VendingMachine
(Context) 持有所有状态对象的实例,并有一个currentState
指向当前状态。它的操作方法(insertCoin
,ejectCoin
,selectItem
)都直接委托给currentState
的同名方法。它还提供了setState
方法供状态对象改变其状态,以及访问库存等数据的方法。- 每个
ConcreteState
类(如NoCoinState
,HasCoinState
)实现了State
接口,只关注在本状态下执行每个操作应该有的行为,以及在满足条件时调用machine.setState()
来切换到下一个状态。 - 客户端只与
VendingMachine
交互,行为的改变看起来就像是VendingMachine
对象自身“改变了类”一样。
五、模式的价值:状态模式带来的清晰与扩展
状态模式的核心价值在于其对状态驱动行为的优雅处理:
- 将与特定状态相关的行为局部化 (Localizes State-Specific Behavior): 每个状态的行为都封装在对应的
ConcreteState
类中,使得代码更加内聚、清晰、易于理解和维护。 - 消除了庞大的条件分支语句 (Eliminates Large Conditional Statements): 将原本分散在 Context 类各个方法中的
if-else
或switch
结构,用状态对象的多态来替代。 - 使得状态转换显式化 (Makes State Transitions Explicit): 状态之间的转换逻辑通常也封装在状态类内部(或者由 Context 集中管理,但不推荐),使得状态机的流转更加清晰可见。
- 符合开闭原则 (Supports Open/Closed Principle): 添加新的状态非常容易,只需创建一个新的
ConcreteState
类,并修改触发转换到新状态的那个状态类的代码即可。Context 类通常无需修改。
六、权衡与考量:状态模式的类数量增加
引入状态模式的主要代价是:
- 增加了类的数量 (Increased Number of Classes): 系统中的每一种状态都需要一个对应的
ConcreteState
类。如果状态非常多,可能会导致类的数量增加。
但相比于维护一个包含大量条件判断的庞大 Context 类,这种将状态行为分散到多个小类中的做法,通常能带来更高的代码质量和可维护性。
七、明辨异同:状态 vs. 策略 (FAQ)
- Q1: 状态模式 (State) vs. 策略模式 (Strategy)?
- A1: (再次强调这个重要区别)
- 意图: Strategy 封装可互换的算法,由客户端选择。State 封装对象基于内部状态的不同行为,状态切换通常由对象内部(或状态对象自身)驱动。
- 状态转换: State 模式的核心之一是处理状态之间的转换。Strategy 模式通常不关心“策略”之间的转换。
- Context 与 State/Strategy 的关系: 在 Strategy 模式中,Context 通常对它所使用的 Strategy 不敏感(只关心接口)。而在 State 模式中,Context 通常知道自己可能处于哪些状态,并且 State 对象经常需要回访 Context 来改变其状态或获取数据。
- A1: (再次强调这个重要区别)
八、心法归纳:行为委托状态,转换封装其中
状态模式的核心“心法”在于**“行为委托状态”与“转换封装其中”**:
- 行为委托状态 (Delegate Behavior to State): 将对象在不同内部状态下的行为,从对象自身(Context)剥离出来,委托给代表当前状态的状态对象去执行。Context 只负责维持当前状态的引用并进行委托。
- 转换封装其中 (Encapsulate Transitions within States): 将状态之间的转换逻辑也主要封装在状态类内部。当某个状态下的行为执行到某个条件满足时,由该状态对象负责调用 Context 的方法,将 Context 的当前状态切换到下一个合适的状态。
掌握状态模式,意味着你拥有了:
- 优雅处理状态驱动行为的强大武器: 告别臃肿的条件判断,让代码清晰、内聚。
- 构建清晰状态机的能力: 让状态转换逻辑显式化、易于管理。
- 实现对象行为动态变化的高级技巧: 对象仿佛能根据状态“改变自己的类”。
- 实践开闭原则,应对状态扩展的有效途径。
当你设计的对象其行为会随内部状态发生复杂变化,并且充满了条件判断逻辑时,状态模式就是你设计工具箱中那把能够理顺逻辑、提升代码质量的“手术刀”。它将状态与行为紧密绑定,通过多态实现了行为的平滑切换,是构建健壮、可维护的状态驱动系统的关键模式。
下一章预告: 《Java 设计模式心法:访问者 (Visitor) - 在不修改结构的前提下增加新操作》。如果我们有一个相对稳定的对象结构(比如一个复杂的树形结构),现在需要为这个结构中的多种不同类型的元素添加新的操作,但又不希望修改这些元素类本身(以符合开闭原则),该怎么办?访问者模式将为我们展示一种巧妙的方法,通过“派遣”访问者在对象结构上“巡视”,来实现操作的添加与分离。敬请期待!