Skip to content

状态模式 (State) —— 拒绝 if-else 地狱

前言

在深入探讨状态模式之前,想向大家推荐一个非常棒的开源项目:design-patterns-23。这个项目用最现代的技术栈重写了 23 种设计模式,非常适合实战学习,还配套了在线交互演示站,可以边读文章边动手玩。本文的实战案例灵感也来源于此。

1. 状态模式是什么?

状态模式(State Pattern)允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。

简单来说,就是对象的行为取决于它当前所处的状态

1.1 核心概念

  • Context(环境类):维护一个 ConcreteState 子类的实例,这个实例定义当前的状态。
  • State(抽象状态):定义一个接口,以封装与 Context 的一个特定状态相关的行为。
  • ConcreteState(具体状态):实现 State 接口,处理该状态下的具体逻辑。

1.2 为什么我们需要它?

场景: 我们来看看最经典的电商订单系统。一个订单可能有多种状态:待支付、已支付、发货中、已完结、已取消。 针对同一个操作(比如“取消订单”),在不同状态下的处理逻辑完全不同:

  • 待支付:直接取消,关闭订单。
  • 已支付:需要触发退款流程,然后关闭订单。
  • 发货中:不能取消,或者需要拦截物流。
  • 已完结:不能取消,只能申请售后。

痛点if-else 地狱 如果不使用状态模式,你的代码很可能会写成这样:

java
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 实战:电商订单状态机

我们来实现一个简单的订单状态流转。

涉及状态

  1. PendingState (待支付)
  2. PaidState (已支付)
  3. ShippedState (已发货)
  4. CompletedState (已完结)

涉及动作

  1. pay() (支付)
  2. ship() (发货)
  3. receive() (收货)

2.1 代码实现

Step 1: 定义状态接口

java
public interface OrderState {
    void pay(OrderContext context);
    void ship(OrderContext context);
    void receive(OrderContext context);
}

Step 2: 定义环境类 (OrderContext) 这也是我们的订单对象本身。

java
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)

java
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)

java
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)

java
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)

java
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: 客户端测试

java
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();
    }
}

输出结果

text
还没给钱呢,发什么货?
支付成功!
当前状态切换为: PaidState
您已经付过钱了,不要重复支付。
商品已发货,请耐心等待。
当前状态切换为: ShippedState
用户已签收,交易完成!
当前状态切换为: CompletedState
订单已完结。

3. 进阶技巧:使用枚举实现状态模式

如果状态类没有复杂的成员变量(无状态的单例),我们可以用 Java 的 enum 来简化代码,这样就不需要创建那么多单独的类文件了。

java
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(动作)。

简单配置示例

java
@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 优点

  1. 封装了转换规则:将状态转换逻辑封装在状态类内部,而不是堆砌在 Context 中。
  2. 消除条件语句:消灭了大量的 if-elseswitch-case
  3. 扩展性好:增加新状态只需要增加一个新的类。

6.2 缺点

  1. 类膨胀:每个状态都是一个类,如果状态机很复杂,类的数量会剧增。
  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),它会随着你的操作在 PlayingPausedStopped 之间切换,并且按钮的可用性(Enabled/Disabled)也会随之动态调整。 截图建议:将播放器置于 Playing 状态,截取此时的界面,重点突出显示当前状态的标签(如 "Current State: Playing")以及高亮或置灰的控制按钮。