告别 if-else
地狱:用状态模式优雅地管理对象状态
在软件开发中,我们经常会遇到这样的问题:一个对象的行为,会随着它自身的状态改变而改变。
想象一个常见的在线订单 系统:一个订单可以处于 新建
、已发货
、已完成
、已取消
等多种状态。在不同的状态下,它能执行的操作也不同:比如,只有在 新建
状态下才能发货,而一旦发货,就不能被取消。
不用状态模式,我们通常怎么做?
在没有设计模式的帮助下,处理这类问题最常见的做法,就是在核心类中用大量的 if-else
或 switch
语句来判断当前状态,然后执行不同的逻辑。
我们来看一个常规的代码实现:
Java
public class Order {
private String status; // 订单状态,比如 "NEW", "SHIPPED"
public Order() {
this.status = "NEW";
}
public void shipOrder() {
// 大量的 if-else 语句来处理不同状态下的发货逻辑
if (this.status.equals("NEW")) {
System.out.println("订单已支付,正在发货...");
this.status = "SHIPPED";
} else if (this.status.equals("SHIPPED")) {
System.out.println("订单已发货,不能重复发货。");
} else if (this.status.equals("COMPLETED")) {
System.out.println("订单已完成,不能再发货。");
}
}
public void cancelOrder() {
// 又一个庞大的 if-else 语句来处理取消逻辑
if (this.status.equals("NEW")) {
System.out.println("订单已取消。");
this.status = "CANCELLED";
} else if (this.status.equals("SHIPPED")) {
System.out.println("订单已发货,不能取消。");
}
}
}
这种代码最初看起来没问题,并且一开始的第一直觉就是这样来实现,但它存在严重的维护问题:
代码臃肿 :所有的状态判断和业务逻辑都集中在Order
类里,导致这个类非常庞大。
违反开闭原则:每增加一个新状态或新操作,都必须回来修改这个核心类,涉及修改就要把这个类全部测一遍,这极大地增加了维护难度和引入 Bug 的风险。
难以理解:随着状态增多,代码变得难以阅读和理解,状态之间的关联关系,排斥关系会变得很难理清楚。
这时,状态模式 登场了。它最核心的思想是:把每一种状态的行为都封装到一个独立的类中,从根本上解决了上述问题。
目前来看还是很简单的逻辑,真实的业务场景更加复杂,还会涉及到状态回退,以及更为致命的流程变更。
一个完整的状态模式 Java 示例
下面,我们将所有组件整合到一个完整的 Java 代码示例中,并配上详细的说明,让你能够轻松理解每个部分。
类图

1. 状态接口(OrderState.java
)
这个接口是状态模式的核心,它定义了订单在不同状态下能执行的所有操作。所有具体的行为都被抽象到这个接口中。
Java
public interface OrderState {
void ship(Order order);
void cancel(Order order);
void complete(Order order);
}
2. 具体状态类
为每种状态创建一个独立的类,将特定状态下的行为逻辑和状态转换封装在各自的类中。
Java
// 新建状态:订单可以被发货或取消
public class NewState implements OrderState {
@Override
public void ship(Order order) {
System.out.println("订单已支付,正在发货...");
// 状态转换:从NewState变为ShippedState
order.setState(new ShippedState());
}
@Override
public void cancel(Order order) {
System.out.println("订单已取消。");
// 状态转换:从NewState变为CancelledState
order.setState(new CancelledState());
}
@Override
public void complete(Order order) {
System.out.println("订单在'新建'状态下无法被完成。");
}
}
// 已发货状态:订单可以被完成,但不能被取消或重复发货
public class ShippedState implements OrderState {
@Override
public void ship(Order order) {
System.out.println("订单已发货,不能重复发货。");
}
@Override
public void cancel(Order order) {
System.out.println("订单已发货,不能取消。");
}
@Override
public void complete(Order order) {
System.out.println("订单已完成。");
// 状态转换
order.setState(new CompletedState());
}
}
// 已完成状态:任何操作都无效
public class CompletedState implements OrderState {
@Override
public void ship(Order order) {
System.out.println("订单已完成,不能再进行任何操作。");
}
@Override
public void cancel(Order order) {
System.out.println("订单已完成,不能再进行任何操作。");
}
@Override
public void complete(Order order) {
System.out.println("订单已完成,不能重复完成。");
}
}
// 已取消状态:任何操作都无效
public class CancelledState implements OrderState {
@Override
public void ship(Order order) {
System.out.println("订单已取消,无法进行任何操作。");
}
@Override
public void cancel(Order order) {
System.out.println("订单已取消,不能重复取消。");
}
@Override
public void complete(Order order) {
System.out.println("订单已取消,无法进行任何操作。");
}
}
3. 上下文类(Order.java
)
这是核心类,它不再包含复杂的 if-else
逻辑。它只负责维护一个当前状态的引用,并将操作委托给它。
Java
import java.util.Objects;
public class Order {
private OrderState state;
public Order() {
this.state = new NewState(); // 初始状态为NewState
}
// 设置状态的方法,供状态类内部调用进行状态转换
public void setState(OrderState state) {
System.out.println("状态从 " + this.state.getClass().getSimpleName() +
" 转换为 " + state.getClass().getSimpleName());
this.state = Objects.requireNonNull(state);
}
// 暴露给外部调用的方法,将请求委托给当前状态对象处理
public void ship() {
this.state.ship(this);
}
public void cancel() {
this.state.cancel(this);
}
public void complete() {
this.state.complete(this);
}
}
4. 测试类(Main.java
)
最后,我们通过一个简单的测试类来运行整个流程,观察状态如何进行转换。
Java
public class Main {
public static void main(String[] args) {
Order order = new Order();
System.out.println("--- 初始状态:新建订单 ---");
order.ship(); // 订单发货
System.out.println("\n--- 状态:已发货 ---");
order.ship(); // 再次发货,操作无效
order.complete(); // 完成订单
System.out.println("\n--- 状态:已完成 ---");
order.cancel(); // 无法取消
}
}
运行结果:
diff
--- 初始状态:新建订单 ---
订单已支付,正在发货...
状态从 NewState 转换为 ShippedState
--- 状态:已发货 ---
订单已发货,不能重复发货。
订单已完成。
状态从 ShippedState 转换为 CompletedState
--- 状态:已完成 ---
订单已完成,不能再进行任何操作。
状态模式的优缺点
优点:
开闭原则:增加一个新状态,只需添加一个新类,无需修改现有代码,我们只需要保证这个类是正常的,大大降低系统因为扩展导致bug的概率。
消除 if-else
:用多态代替了复杂的条件判断,代码更清晰、更易读,状态逻辑关系也更好梳理。代码维护和流程变更的情况处理起来更加友好。
更好的封装:每个状态的逻辑都独立封装,降低了耦合。
缺点:
类的数量增加:对于简单的状态机来说,可能会导致类太多,显得过度设计。
代码结构变复杂:对于初学者来说,这种多类协作的方式可能更难理解。
状态模式 vs. 策略模式
虽然两者结构相似,但目的和驱动力完全不同。
特性 | 状态模式 (State) | 策略模式 (Strategy) |
---|---|---|
目的 | 解决对象行为随其内部状态而改变的问题。 | 解决可互换算法的问题,允许运行时选择不同的行为。 |
行为改变的驱动力 | 由对象内部控制。一个状态对象通常会负责把上下文(Context)切换到下一个状态。 | 由客户端外部控制。客户端选择并配置一个具体策略,然后执行。 |
场景 | 行为是动态、递进的,如订单的生命周期、红绿灯状态。 | 行为是静态、可选择的,如支付方式、排序算法。 |
我们可以用一个游戏角色的攻击系统来巩固一下两者区别
假设角色有 普通攻击
、魔法攻击
和弓箭攻击
三种方式。这些攻击方式是由玩家 (也就是外部客户端)选择的,这种可供选择、可随时切换的行为,非常适合用策略模式来实现。每种攻击方式都是一种策略。
而状态模式 则更适合处理角色在站立
、跳跃
、奔跑
等不同状态下的行为。角色进入"跳跃"状态后,其行为(比如移动方式、可用技能)会由角色自身根据内部状态自动改变,而不是由外部决定。
状态模式在常用开发框架中的应用
状态模式在许多主流的开发框架中都有实际应用,只是表现形式有所不同。
- Spring State Machine: 这是 Spring 框架中一个专门用于实现状态机模式的子项目。它提供了丰富的注解和配置,让开发者可以非常便捷地定义状态、事件和转换,非常适合复杂的业务流程(如订单处理、审批流程)。
- 工作流/BPM 引擎: 许多业务流程管理(BPM)引擎(如 Activiti, Camunda)的核心就是状态机。一个流程实例的生命周期,就是从一个状态转换到另一个状态,每个状态都有特定的处理逻辑。