你的订单状态机写成 switch-case 了?State 模式跟策略模式差了一个维度

每个后端系统里都有状态机。订单状态、支付状态、审批流状态、会员状态------只要有生命周期的地方就有状态。

但大多数 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 模式是用卡皮巴拉换装来比喻的------穿睡衣就是睡觉状态只能躺平,穿工装就是工作状态能搬砖,状态决定了能干什么不能干什么。比看代码直观不少,感兴趣可以搜搜看。

相关推荐
Zane19941 小时前
线程池进阶:如何科学设置线程数(结合实际业务场景复盘)
java·后端
晴殇i1 小时前
用 TRAE Work 搞定客户项目介绍,开发再也不用头疼写汇报 PPT
前端·后端
Cache技术分享1 小时前
488. Java 反射 - 动态创建数组
前端·后端
苏三说技术1 小时前
LangChain4j 入门指南
后端
Arvid1 小时前
MCP 协议史上最大更新:从有状态走向无状态
后端
程序员爱钓鱼2 小时前
Go 布尔类型 bool 详解
后端·面试·go
程序员爱钓鱼2 小时前
Rust Clone详解:深拷贝、复制成本与正确使用方式
后端·面试·rust
程序员-Benothing2 小时前
MySQL 的存储引擎有哪些?它们之间有什么区别?
后端·mysql·面试·职场和发展