状态模式 vs 策略模式

意图:允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。

为什么需要状态模式? 订单、工单、审批流等业务场景中,对象在不同状态下有不同的行为规则。最简单的实现方式是 if-else 判断状态,但当状态超过 5 个时,代码会变成不可维护的意大利面条。

UML 简述:Context 持有当前 State,State 对象决定何时流转到下一个状态。

订单状态机实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 状态接口
public interface OrderState {
void pay(OrderContext ctx); // 支付
void ship(OrderContext ctx); // 发货
void confirm(OrderContext ctx); // 确认收货
void cancel(OrderContext ctx); // 取消
}

// 待支付状态
public class PendingState implements OrderState {
@Override
public void pay(OrderContext ctx) {
System.out.println("支付成功");
ctx.setState(new ShippedState()); // 状态流转
}

@Override
public void ship(OrderContext ctx) {
throw new IllegalStateException("待支付状态不能发货");
}

@Override
public void cancel(OrderContext ctx) {
System.out.println("订单已取消");
ctx.setState(new CancelledState());
}
// confirm() 同理抛异常...
}

// 上下文——持有当前状态
public class OrderContext {
private OrderState state = new PendingState(); // 初始状态

public void setState(OrderState state) {
this.state = state;
}

public void pay() { state.pay(this); }
public void ship() { state.ship(this); }
public void confirm(){ state.confirm(this); }
public void cancel() { state.cancel(this); }
}

状态模式 vs 策略模式的本质区别

这是另一个常见混淆点。两者的类结构几乎相同,但意图完全不同:

维度 状态模式 策略模式
关注点 对象内部状态变化导致行为变化 对象外部算法的替换
谁决定切换 状态对象自己决定何时流转 由调用方/上下文决定使用哪个策略
切换频率 频繁,且有严格的转换规则 通常在初始化时选定,运行时很少变
相互感知 状态对象持有对上下文的引用 策略对象通常不知道上下文的存在

Spring State Machine:对于复杂流程,Spring 提供了专业的状态机框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
@EnableStateMachine
public class OrderStateMachineConfig
extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states) {
states.withStates()
.initial("PENDING")
.states(new HashSet<>(Arrays.asList(
"PENDING", "PAID", "SHIPPED", "COMPLETED", "CANCELLED")));
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) {
transitions
.withExternal().source("PENDING").target("PAID").event("PAY")
.and()
.withExternal().source("PAID").target("SHIPPED").event("SHIP");
}
}