每个后端系统里都有状态机。订单状态、支付状态、审批流状态、会员状态------只要有生命周期的地方就有状态。
但大多数 Java 项目里的状态机长这样:
java
public void handleOrder(Order order, String action) {
switch (order.getStatus()) {
case "CREATED":
if (action.equals("pay")) {
order.setStatus("PAID");
} else if (action.equals("cancel")) {
order.setStatus("CANCELLED");
} else {
throw new IllegalStateException("非法操作");
}
break;
case "PAID":
if (action.equals("ship")) {
order.setStatus("SHIPPED");
} else if (action.equals("refund")) {
order.setStatus("REFUNDED");
}
break;
case "SHIPPED":
if (action.equals("deliver")) {
order.setStatus("DELIVERED");
}
break;
// ... 每加一个状态,这里就多一截
}
}
三个状态还好,等你有 8 个状态、15 种动作,这个 switch 就成了项目里最大的函数。而且每加一个状态,所有 case 分支都要改------因为你不知道哪些动作在新状态下该不该允许。
这就是 State 模式要解决的问题。
State 模式不是策略模式的亲戚
很多人把 State 和 Strategy 归为一类,因为 UML 图长得几乎一样:都是一个接口,多个实现类,一个 Context 持有引用。
区别在于一个维度:谁来决定行为的变化。
Strategy 模式:外部调用方决定用哪个策略。策略之间的切换是调用方的事,策略对象本身不关心其他策略的存在。
java
// Strategy:调用方选策略
PaymentStrategy strategy = payType == ALIPAY
? new AlipayStrategy()
: new WechatStrategy();
strategy.pay(amount);
// 策略之间互不知道对方存在
State 模式:状态对象自己知道下一个状态是什么。状态转移的逻辑封装在状态对象内部,不是外部 switch。
java
// State:状态自己决定转移
public class CreatedState implements OrderState {
public void pay(OrderContext ctx) {
ctx.setState(new PaidState()); // 自己知道转移到哪
}
public void cancel(OrderContext ctx) {
ctx.setState(new CancelledState());
}
public void ship(OrderContext ctx) {
throw new IllegalStateException("未支付订单不能发货");
}
}
核心差异:Strategy 是"算法选择"问题,State 是"状态转移"问题。Strategy 的策略之间是平行的、可互换的;State 的状态之间是有顺序的、有约束的。
搞混了这两个,你写出来的代码就会出现"状态对象互相不知道对方存在"的尴尬------状态 A 不知道能转到状态 B,每次转移还得外部来协调,那 State 模式就退化成了一个更啰嗦的 if-else。
订单状态机的完整实现
来看一个完整的电商订单状态机,状态包括:CREATED → PAID → SHIPPED → DELIVERED → COMPLETED,以及分支状态 CANCELLED、REFUNDED。
先定义状态接口:
java
public interface OrderState {
void pay(OrderContext ctx);
void cancel(OrderContext ctx);
void ship(OrderContext ctx);
void deliver(OrderContext ctx);
void complete(OrderContext ctx);
void refund(OrderContext ctx);
String getName();
}
每个状态自己实现所有动作------合法的执行转移,非法的抛异常:
java
public class CreatedState implements OrderState {
public String getName() { return "CREATED"; }
public void pay(OrderContext ctx) {
ctx.setState(new PaidState());
ctx.notifyEvent("ORDER_PAID");
}
public void cancel(OrderContext ctx) {
ctx.setState(new CancelledState());
ctx.notifyEvent("ORDER_CANCELLED");
}
public void ship(OrderContext ctx) {
throw new IllegalStateException("未支付订单不能发货");
}
public void deliver(OrderContext ctx) {
throw new IllegalStateException("未支付订单不能确认收货");
}
public void complete(OrderContext ctx) {
throw new IllegalStateException("未支付订单不能完成");
}
public void refund(OrderContext ctx) {
throw new IllegalStateException("未支付订单不能退款");
}
}
java
public class PaidState implements OrderState {
public String getName() { return "PAID"; }
public void pay(OrderContext ctx) {
throw new IllegalStateException("订单已支付,不能重复支付");
}
public void cancel(OrderContext ctx) {
// 已支付订单取消 → 走退款流程
ctx.setState(new RefundedState());
ctx.notifyEvent("ORDER_REFUNDED");
}
public void ship(OrderContext ctx) {
ctx.setState(new ShippedState());
ctx.notifyEvent("ORDER_SHIPPED");
}
public void deliver(OrderContext ctx) {
throw new IllegalStateException("未发货订单不能确认收货");
}
public void complete(OrderContext ctx) {
throw new IllegalStateException("未发货订单不能完成");
}
public void refund(OrderContext ctx) {
ctx.setState(new RefundedState());
ctx.notifyEvent("ORDER_REFUNDED");
}
}
Context 持有当前状态,把所有操作委托给状态对象:
java
public class OrderContext {
private OrderState state;
private String orderId;
public OrderContext(String orderId) {
this.orderId = orderId;
this.state = new CreatedState();
}
public void setState(OrderState state) {
this.state = state;
}
public void pay() { state.pay(this); }
public void cancel() { state.cancel(this); }
public void ship() { state.ship(this); }
public void deliver() { state.deliver(this); }
public void complete() { state.complete(this); }
public void refund() { state.refund(this); }
public String getStatus() { return state.getName(); }
private void notifyEvent(String event) {
// 发领域事件
EventBus.publish(new OrderEvent(orderId, event));
}
}
调用方的代码变得极其干净:
java
OrderContext order = new OrderContext("ORD-001");
order.pay(); // CREATED → PAID
order.ship(); // PAID → SHIPPED
order.deliver(); // SHIPPED → DELIVERED
order.complete();// DELIVERED → COMPLETED
// 非法操作直接抛异常
OrderContext order2 = new OrderContext("ORD-002");
order2.ship(); // IllegalStateException: 未支付订单不能发货
没有 switch,没有 if-else。每加一个新状态,只需要写一个新的状态类,不需要改任何现有状态的代码------因为新状态自然不跟旧状态冲突,旧状态里的非法操作检查也不需要改。
三个工程坑
State 模式理论上很优雅,但落地有三个坑。
坑一:状态对象的无状态性
上面的代码里,每次状态转移都 new 一个新状态对象。这在简单场景下没问题,但如果状态对象需要持有上下文信息(比如"已重试 3 次"),就会出问题。
java
// 错误写法:状态对象持有可变状态
public class ProcessingState implements OrderState {
private int retryCount = 0; // 这个变量在 new 出来时就丢了
public void retry(OrderContext ctx) {
retryCount++; // 每次进来都是新对象,retryCount 永远是 1
if (retryCount >= 3) {
ctx.setState(new FailedState());
}
}
}
解决方案:把状态相关数据放在 Context 里,状态对象只负责行为逻辑。
java
public class ProcessingState implements OrderState {
public void retry(OrderContext ctx) {
int retryCount = ctx.getRetryCount() + 1;
ctx.setRetryCount(retryCount);
if (retryCount >= 3) {
ctx.setState(new FailedState());
}
}
}
状态对象必须是无状态的。这一条搞不定,所有状态对象都变成有状态的,整个模式就退化成了带状态的 if-else。
坑二:状态爆炸
电商订单的核心状态可能只有 6 个,但加上"部分发货""部分退款""换货中""待审核"这些中间状态,轻松到 20 个以上。每个状态都要实现接口的所有方法------假设接口有 8 个方法,20 个状态就是 160 个方法实现,其中大部分是 throw new IllegalStateException。
这时候 State 模式的代码量反而比 switch-case 还多。
解决方案有两个方向:
方向一:用状态机框架。Spring StateMachine 是 Spring 生态里的状态机实现,用配置声明状态和转移规则,不用手写状态类:
java
@Configuration
@EnableStateMachine
public class OrderStateMachineConfig extends StateMachineConfigurerAdapter<OrderState, OrderEvent> {
@Override
public void configure(StateMachineStateConfigurer<OrderState, OrderEvent> states) {
states
.withStates()
.initial(OrderState.CREATED)
.states(EnumSet.allOf(OrderState.class))
.end(OrderState.COMPLETED)
.end(OrderState.CANCELLED);
}
@Override
public void configure(StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions) {
transitions
.withExternal()
.source(OrderState.CREATED).target(OrderState.PAID)
.event(OrderEvent.PAY)
.and()
.withExternal()
.source(OrderState.PAID).target(OrderState.SHIPPED)
.event(OrderEvent.SHIP)
.and()
.withExternal()
.source(OrderState.PAID).target(OrderState.REFUNDED)
.event(OrderEvent.REFUND);
// 继续配置...
}
}
方向二:回到 switch-case,但用枚举 + 转移表代替嵌套 if-else:
java
// 状态转移表:key = 当前状态, value = {事件 → 目标状态}
private static final Map<OrderState, Map<OrderEvent, OrderState>> TRANSITIONS = Map.of(
CREATED, Map.of(PAY, PAID, CANCEL, CANCELLED),
PAID, Map.of(SHIP, SHIPPED, REFUND, REFUNDED),
SHIPPED, Map.of(DELIVER, DELIVERED),
DELIVERED, Map.of(COMPLETE, COMPLETED)
);
public void handleEvent(OrderContext ctx, OrderEvent event) {
Map<OrderEvent, OrderState> transitions = TRANSITIONS.get(ctx.getState());
if (transitions == null || !transitions.containsKey(event)) {
throw new IllegalStateException(
String.format("状态 %s 不允许操作 %s", ctx.getState(), event)
);
}
ctx.setState(transitions.get(event));
}
状态超过 10 个用框架,5 个以内手写 State 模式,5-10 个用转移表。这不是妥协,是工程判断。
坑三:状态持久化与恢复
订单状态最终要落库。数据库里存的是状态名字符串("PAID"),但内存里的状态是一个对象。每次从数据库恢复订单时,需要把字符串转回状态对象。
java
// 从数据库恢复订单
public OrderContext restoreOrder(OrderDO orderDO) {
OrderContext ctx = new OrderContext(orderDO.getOrderId());
OrderState state = switch (orderDO.getStatus()) {
case "CREATED" -> new CreatedState();
case "PAID" -> new PaidState();
case "SHIPPED" -> new ShippedState();
case "DELIVERED" -> new DeliveredState();
case "COMPLETED" -> new CompletedState();
case "CANCELLED" -> new CancelledState();
case "REFUNDED" -> new RefundedState();
default -> throw new IllegalStateException("未知状态: " + orderDO.getStatus());
};
ctx.setState(state);
return ctx;
}
又回到 switch 了。这是 State 模式的原罪------内存里的多态对象和持久化时的扁平字符串之间的阻抗不匹配。
实际项目中的做法:如果状态对象是无状态的(应该是),那就用枚举单例代替 new,恢复时直接通过枚举值映射:
java
public enum OrderStateEnum implements OrderState {
CREATED {
public void pay(OrderContext ctx) { ctx.setState(PAID); }
public void cancel(OrderContext ctx) { ctx.setState(CANCELLED); }
// 其他方法默认抛异常
},
PAID {
public void ship(OrderContext ctx) { ctx.setState(SHIPPED); }
public void refund(OrderContext ctx) { ctx.setState(REFUNDED); }
},
SHIPPED {
public void deliver(OrderContext ctx) { ctx.setState(DELIVERED); }
};
// 默认实现:所有未覆写的方法抛异常
public void pay(OrderContext ctx) { throw illegalState("pay"); }
public void cancel(OrderContext ctx) { throw illegalState("cancel"); }
public void ship(OrderContext ctx) { throw illegalState("ship"); }
public void deliver(OrderContext ctx) { throw illegalState("deliver"); }
public void complete(OrderContext ctx) { throw illegalState("complete"); }
public void refund(OrderContext ctx) { throw illegalState("refund"); }
}
// 恢复时:直接用枚举
ctx.setState(OrderStateEnum.valueOf(orderDO.getStatus()));
枚举单例解决了 new 的问题,也解决了持久化恢复的问题,还天然是线程安全的。唯一代价是枚举类会比较大------但这比 20 个独立类文件好维护多了。
什么时候别用 State 模式
不是所有有 if-else 的地方都该上 State 模式。判断标准:
状态数量少(≤3)且不会增长:直接 if-else。引入模式增加的复杂度比它解决的问题还大。
状态没有转移约束:如果任何状态下都可以执行任何操作(操作之间是正交的不是有顺序的),那这不是状态机,是策略选择------用 Strategy。
状态转移逻辑由外部系统驱动:如果状态变化是由数据库触发器、消息队列、定时任务等外部系统驱动的,状态对象内部无法感知转移时机,State 模式的"自驱动转移"优势就发挥不出来。
一个真实的对比
我重构过一个订单系统,原来是一个 800 行的 OrderService,里面一个 handleAction 方法 400 行 switch-case,覆盖 7 个状态 12 种动作。每次加新状态,QA 要回归全部流程,因为你不知道改了哪个 case 会不会影响其他状态。
重构后用枚举 + State 模式,7 个枚举值各自覆写需要的方法,加新状态就是加一个枚举值,不需要动其他状态的代码。单元测试也从"测一个 400 行函数"变成了"每个状态独立测"------测试用例数量没变,但每个用例的复杂度从"设置 5 个前置条件"降到"设置 1 个状态"。
代价是文件变多了。7 个状态的枚举类有 300 行,加上 Context 和接口,总共 500 行。比原来的 400 行 switch 多了 100 行。但这 500 行里,每个状态的逻辑是独立的、可测试的、可扩展的------而那 400 行 switch 里,所有状态的逻辑搅在一起,改一个分支要读全部代码才能确认不会影响其他分支。
这就是 State 模式的价值:不是代码量更少,是变更的影响半径更小。
顺便提一句,我在做的「爪爪代码冒险记」小程序里,State 模式是用卡皮巴拉换装来比喻的------穿睡衣就是睡觉状态只能躺平,穿工装就是工作状态能搬砖,状态决定了能干什么不能干什么。比看代码直观不少,感兴趣可以搜搜看。