状态模式 (State) —— 拒绝 if-else 地狱
前言
在深入探讨状态模式之前,想向大家推荐一个非常棒的开源项目:design-patterns-23。这个项目用最现代的技术栈重写了 23 种设计模式,非常适合实战学习,还配套了在线交互演示站,可以边读文章边动手玩。本文的实战案例灵感也来源于此。
1. 状态模式是什么?
状态模式(State Pattern)允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
简单来说,就是对象的行为取决于它当前所处的状态。
1.1 核心概念
- Context(环境类):维护一个 ConcreteState 子类的实例,这个实例定义当前的状态。
- State(抽象状态):定义一个接口,以封装与 Context 的一个特定状态相关的行为。
- ConcreteState(具体状态):实现 State 接口,处理该状态下的具体逻辑。
1.2 为什么我们需要它?
场景: 我们来看看最经典的电商订单系统。一个订单可能有多种状态:待支付、已支付、发货中、已完结、已取消。 针对同一个操作(比如“取消订单”),在不同状态下的处理逻辑完全不同:
- 待支付:直接取消,关闭订单。
- 已支付:需要触发退款流程,然后关闭订单。
- 发货中:不能取消,或者需要拦截物流。
- 已完结:不能取消,只能申请售后。
痛点:if-else 地狱 如果不使用状态模式,你的代码很可能会写成这样:
public void cancelOrder() {
if (state == PENDING) {
// 逻辑A
} else if (state == PAID) {
// 逻辑B
} else if (state == SHIPPED) {
// 逻辑C
} else if (state == COMPLETED) {
// 逻辑D
}
}这种代码不仅难看,而且极难维护。每次新增一个状态(比如“退款中”),你都得去改所有的方法,在每个 if-else 里插一脚,很容易改出 Bug。
解决方案: 状态模式将每个状态的逻辑封装到独立的类中。Context 只要把请求委托给当前的 State 对象即可。
2. Java 实战:电商订单状态机
我们来实现一个简单的订单状态流转。
涉及状态:
PendingState(待支付)PaidState(已支付)ShippedState(已发货)CompletedState(已完结)
涉及动作:
pay()(支付)ship()(发货)receive()(收货)
2.1 代码实现
Step 1: 定义状态接口
public interface OrderState {
void pay(OrderContext context);
void ship(OrderContext context);
void receive(OrderContext context);
}Step 2: 定义环境类 (OrderContext) 这也是我们的订单对象本身。
public class OrderContext {
private OrderState state;
// 默认是待支付状态
public OrderContext() {
this.state = new PendingState();
}
public void setState(OrderState state) {
this.state = state;
System.out.println("当前状态切换为: " + state.getClass().getSimpleName());
}
public OrderState getState() {
return state;
}
// 动作委托
public void pay() { state.pay(this); }
public void ship() { state.ship(this); }
public void receive() { state.receive(this); }
}Step 3: 实现具体状态
待支付状态 (PendingState)
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 receive(OrderContext context) {
System.out.println("你还没买呢,收什么货?");
}
}已支付状态 (PaidState)
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 receive(OrderContext context) {
System.out.println("还在发货中,暂时不能收货。");
}
}已发货状态 (ShippedState)
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 receive(OrderContext context) {
System.out.println("用户已签收,交易完成!");
// 状态流转:已发货 -> 已完结
context.setState(new CompletedState());
}
}已完结状态 (CompletedState)
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 receive(OrderContext context) {
System.out.println("订单已完结。");
}
}Step 4: 客户端测试
public class Client {
public static void main(String[] args) {
OrderContext order = new OrderContext();
// 1. 刚下单,试图发货(失败)
order.ship();
// 2. 支付
order.pay();
// 3. 支付后,再次支付(失败)
order.pay();
// 4. 发货
order.ship();
// 5. 收货
order.receive();
// 6. 完结后试图操作
order.ship();
}
}输出结果:
还没给钱呢,发什么货?
支付成功!
当前状态切换为: PaidState
您已经付过钱了,不要重复支付。
商品已发货,请耐心等待。
当前状态切换为: ShippedState
用户已签收,交易完成!
当前状态切换为: CompletedState
订单已完结。3. 进阶技巧:使用枚举实现状态模式
如果状态类没有复杂的成员变量(无状态的单例),我们可以用 Java 的 enum 来简化代码,这样就不需要创建那么多单独的类文件了。
public enum OrderStateEnum {
PENDING {
@Override
public void pay(OrderContext2 context) {
System.out.println("支付成功");
context.setState(PAID);
}
// 其他方法空实现或抛异常
},
PAID {
@Override
public void ship(OrderContext2 context) {
System.out.println("发货成功");
context.setState(SHIPPED);
}
},
SHIPPED {
@Override
public void receive(OrderContext2 context) {
System.out.println("收货成功");
context.setState(COMPLETED);
}
},
COMPLETED;
// 默认实现(防止重复代码)
public void pay(OrderContext2 context) { System.out.println("当前状态无法支付"); }
public void ship(OrderContext2 context) { System.out.println("当前状态无法发货"); }
public void receive(OrderContext2 context) { System.out.println("当前状态无法收货"); }
}4. 状态模式 vs 策略模式
这两个模式的 UML 类图简直一模一样,但核心意图完全不同:
| 特性 | 状态模式 (State) | 策略模式 (Strategy) |
|---|---|---|
| 关注点 | 状态流转 | 算法替换 |
| 谁来切换 | 通常由状态对象自己控制流转到下一个状态 | 通常由客户端指定使用哪个策略 |
| 感知度 | Client 通常不知道 State 的存在(只和 Context 交互) | Client 必须知道具体的 Strategy 才能选择 |
| 生命周期 | 状态会随着时间动态变化 | 策略通常在对象创建时就定好了,运行期不常变 |
一句话总结:
- 策略模式:你怎么去北京?(坐飞机、坐高铁、自驾)—— 条条大路通罗马。
- 状态模式:你现在几岁?(童年、青年、老年)—— 随着时间自动演变。
5. 企业级应用:Spring Statemachine
在实际的大型企业项目中,手写状态模式可能还不够用(比如涉及复杂的状态分层、并发控制、持久化等)。这时推荐使用成熟的框架:Spring Statemachine。
它支持:
- 基于注解的配置。
- ZooKeeper/Redis 状态持久化。
- 复杂的 Guard(守卫条件)和 Action(动作)。
简单配置示例:
@Configuration
@EnableStateMachine
public class Config extends EnumStateMachineConfigurerAdapter<States, Events> {
@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
throws Exception {
states
.withStates()
.initial(States.PENDING)
.state(States.PAID)
.state(States.SHIPPED)
.end(States.COMPLETED);
}
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
throws Exception {
transitions
.withExternal()
.source(States.PENDING).target(States.PAID).event(Events.PAY)
.and()
.withExternal()
.source(States.PAID).target(States.SHIPPED).event(Events.SHIP);
}
}6. 优缺点总结
6.1 优点
- 封装了转换规则:将状态转换逻辑封装在状态类内部,而不是堆砌在 Context 中。
- 消除条件语句:消灭了大量的
if-else或switch-case。 - 扩展性好:增加新状态只需要增加一个新的类。
6.2 缺点
- 类膨胀:每个状态都是一个类,如果状态机很复杂,类的数量会剧增。
- 实现复杂:虽然逻辑清晰了,但代码结构变得复杂了,对于简单的只有两个状态的场景,用
if-else反而更直观。
7. 总结
状态模式是处理复杂对象生命周期的神器。
- 如果你的对象只有 2-3 个状态,用
if-else没问题。 - 如果状态超过 3 个,且状态之间有流转关系(A -> B -> C),请务必考虑状态模式。
- 如果非常复杂,直接上 Spring Statemachine。
8. 实验实操
在 design-patterns-web 项目中,我们模拟了一个音频播放器的状态机。 这个 Demo 直观地展示了对象行为如何随状态改变:
- 状态约束:当播放器处于
Playing状态时,点击 "Play" 按钮不会有任何反应(或者提示已在播放);只有点击 "Pause" 或 "Stop" 才会触发状态流转。 - 行为变化:观察控制面板上的状态标签(State Label),它会随着你的操作在
Playing、Paused、Stopped之间切换,并且按钮的可用性(Enabled/Disabled)也会随之动态调整。 截图建议:将播放器置于Playing状态,截取此时的界面,重点突出显示当前状态的标签(如 "Current State: Playing")以及高亮或置灰的控制按钮。