状态模式:状态转换、消除状态判断、实战:订单状态机
状态模式,说白了就是让对象在内部状态改变时,自动切换行为。听起来有点抽象?我举个例子你就明白了。
你想想看,一个订单系统里,订单的状态有:待支付、已支付、已发货、已完成、已取消。每个状态下,用户能做的操作完全不同。比如待支付状态下可以取消,已支付状态下就不能取消了。传统写法是什么?一堆 if-else 或者 switch-case,对吧?
这种写法的问题很明显:每增加一个状态,就要改一大片代码。我在项目中遇到过这种痛,改一个状态逻辑,结果影响了三个地方,测试回归了整整两天。嗯,从那以后,我对状态模式就特别上心。
状态模式的核心思想
状态模式的核心,就是把每个状态封装成一个独立的类。每个类只负责自己状态下的行为逻辑。这样,状态转换就变成了对象替换——说白了,就是换一个状态对象。
我个人的习惯是,先画一张状态转换图,把所有的状态和允许的转换路径理清楚。这一步做不好,后面写代码就是给自己挖坑。
状态模式三要素:
- 上下文(Context):持有当前状态对象,对外暴露行为接口
- 抽象状态(State):定义所有状态共有的行为接口
- 具体状态(ConcreteState):实现特定状态下的行为逻辑
消除状态判断:从 if-else 到多态
传统写法里,状态判断长这样:
// 传统写法:满屏的 if-else
public void cancelOrder(Order order) {
if (order.getStatus() == OrderStatus.PENDING_PAYMENT) {
// 待支付可以取消
order.setStatus(OrderStatus.CANCELLED);
} else if (order.getStatus() == OrderStatus.PAID) {
// 已支付需要退款
refund(order);
order.setStatus(OrderStatus.CANCELLED);
} else if (order.getStatus() == OrderStatus.SHIPPED) {
// 已发货不能取消
throw new IllegalStateException("已发货订单不能取消");
}
// ... 继续 else if
}
这种代码,每加一个状态就要加一个 else if。而且多个操作(取消、支付、发货)都要写类似的判断逻辑。维护起来,真的会让人崩溃。
用状态模式改写之后:
// 抽象状态接口
public interface OrderState {
void pay(OrderContext context);
void cancel(OrderContext context);
void ship(OrderContext context);
void complete(OrderContext context);
}
// 待支付状态
public class PendingPaymentState implements OrderState {
@Override
public void pay(OrderContext context) {
System.out.println("支付成功,进入已支付状态");
context.setState(new PaidState());
}
@Override
public void cancel(OrderContext context) {
System.out.println("取消订单,进入已取消状态");
context.setState(new CancelledState());
}
@Override
public void ship(OrderContext context) {
throw new IllegalStateException("未支付不能发货");
}
@Override
public void complete(OrderContext context) {
throw new IllegalStateException("未支付不能完成");
}
}
// 已支付状态
public class PaidState implements OrderState {
@Override
public void pay(OrderContext context) {
throw new IllegalStateException("已支付,不能重复支付");
}
@Override
public void cancel(OrderContext context) {
System.out.println("退款处理中...");
context.setState(new CancelledState());
}
@Override
public void ship(OrderContext context) {
System.out.println("发货成功,进入已发货状态");
context.setState(new ShippedState());
}
@Override
public void complete(OrderContext context) {
throw new IllegalStateException("未发货不能完成");
}
}
我的经验:状态模式最爽的地方在于,每个状态类只关心自己允许的操作。不允许的操作直接抛异常,逻辑清晰到不行。我曾经在一个电商项目中用这种方式重构了订单模块,代码量减少了40%,bug率直线下降。
实战:订单状态机
好了,理论说完了,咱们直接上一个完整的订单状态机。这个例子我用了很多次,每次讲状态模式都拿它当案例。
先定义状态转换表:
| 当前状态 | 支付 | 取消 | 发货 | 完成 |
|---|---|---|---|---|
| 待支付 | → 已支付 | → 已取消 | ❌ | ❌ |
| 已支付 | ❌ | → 已取消(退款) | → 已发货 | ❌ |
| 已发货 | ❌ | ❌ | ❌ | → 已完成 |
| 已完成 | ❌ | ❌ | ❌ | ❌ |
| 已取消 | ❌ | ❌ | ❌ | ❌ |
接下来是上下文类:
public class OrderContext {
private OrderState currentState;
public OrderContext() {
// 初始状态:待支付
this.currentState = new PendingPaymentState();
}
public void setState(OrderState state) {
this.currentState = state;
}
public void pay() {
currentState.pay(this);
}
public void cancel() {
currentState.cancel(this);
}
public void ship() {
currentState.ship(this);
}
public void complete() {
currentState.complete(this);
}
}
使用起来非常直观:
public class Demo {
public static void main(String[] args) {
OrderContext order = new OrderContext();
// 待支付 -> 支付 -> 已支付
order.pay();
// 已支付 -> 发货 -> 已发货
order.ship();
// 已发货 -> 完成 -> 已完成
order.complete();
// 已完成状态下尝试取消,会抛异常
// order.cancel(); // IllegalStateException
}
}
避坑指南:我曾经犯过一个错误——在状态类里直接操作数据库。记住,状态类只负责状态转换逻辑,不要在里面写持久化代码。正确的做法是:状态类返回转换结果,由上下文或外部服务处理持久化。否则,你的状态类会变得又臭又硬,难以测试。
状态模式的优缺点
说实话,状态模式也不是万能的。我总结了一下它的优缺点:
- 优点:
- 消除了大量的条件判断语句
- 每个状态独立,符合单一职责原则
- 新增状态非常方便,开闭原则的完美体现
- 状态转换逻辑集中,容易维护
- 缺点:
- 状态类数量会增多,如果状态很少,反而过度设计
- 状态类之间可能有依赖,比如取消状态需要知道退款逻辑
- 状态转换的复杂度如果很高,状态类之间的交互会变得复杂
我的建议:如果你的状态数量超过3个,而且每个状态下的行为差异很大,那就果断用状态模式。如果只有2个状态,或者状态之间行为差异很小,用简单的 if-else 反而更清晰。别为了用模式而用模式,这是很多新手容易犯的毛病。
最后说一句,状态模式在实际项目中用得非常多。除了订单系统,工作流引擎、游戏角色状态、网络连接状态,都是它的典型应用场景。你想想看,这些场景是不是都有共同点?状态多、转换规则复杂、每个状态行为不同。嗯,这就是状态模式的用武之地。