状态模式:让对象的行为随状态而变

状态模式,说白了就是让一个对象在内部状态改变时,它的行为也跟着变。听起来像不像一个对象在玩变脸?嗯,差不多就是这个意思。

我在做电商系统的时候,订单状态流转这块儿,一开始用了一堆if-else,后来维护起来简直想哭。直到我用了状态模式,整个世界清净了。

状态模式的核心三要素

状态模式有三个主角:状态接口、具体状态、上下文。我一个个说。

1. 状态接口(State)

定义所有具体状态必须实现的方法。说白了就是一个契约,所有状态都得遵守。

// Java 版本
public interface OrderState {
    void pay(Long orderId);
    void cancel(Long orderId);
    void ship(Long orderId);
    void confirm(Long orderId);
}
// C++ 版本
class OrderState {
public:
    virtual ~OrderState() = default;
    virtual void pay(long orderId) = 0;
    virtual void cancel(long orderId) = 0;
    virtual void ship(long orderId) = 0;
    virtual void confirm(long orderId) = 0;
};

2. 具体状态(ConcreteState)

每个具体状态实现状态接口,并且只关心自己状态下的行为逻辑。比如「待支付」状态只处理支付和取消,其他操作直接抛异常。

// Java 版本
public class UnpaidState implements OrderState {
    @Override
    public void pay(Long orderId) {
        System.out.println("订单 " + orderId + " 支付成功,状态变为待发货");
        // 这里会切换上下文的状态
    }

    @Override
    public void cancel(Long orderId) {
        System.out.println("订单 " + orderId + " 已取消");
    }

    @Override
    public void ship(Long orderId) {
        throw new UnsupportedOperationException("未支付不能发货");
    }

    @Override
    public void confirm(Long orderId) {
        throw new UnsupportedOperationException("未支付不能确认收货");
    }
}
// C++ 版本
class UnpaidState : public OrderState {
public:
    void pay(long orderId) override {
        std::cout << "订单 " << orderId << " 支付成功,状态变为待发货" << std::endl;
    }

    void cancel(long orderId) override {
        std::cout << "订单 " << orderId << " 已取消" << std::endl;
    }

    void ship(long orderId) override {
        throw std::runtime_error("未支付不能发货");
    }

    void confirm(long orderId) override {
        throw std::runtime_error("未支付不能确认收货");
    }
};

3. 上下文(Context)

上下文持有当前状态的引用,并把所有请求委托给当前状态对象。它负责状态之间的切换。

// Java 版本
public class OrderContext {
    private OrderState currentState;

    public OrderContext() {
        // 初始状态为待支付
        this.currentState = new UnpaidState();
    }

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

    public void pay(Long orderId) {
        currentState.pay(orderId);
        // 支付成功后切换到待发货状态
        if (currentState instanceof UnpaidState) {
            setState(new PaidState());
        }
    }

    public void cancel(Long orderId) {
        currentState.cancel(orderId);
    }

    public void ship(Long orderId) {
        currentState.ship(orderId);
        if (currentState instanceof PaidState) {
            setState(new ShippedState());
        }
    }

    public void confirm(Long orderId) {
        currentState.confirm(orderId);
        if (currentState instanceof ShippedState) {
            setState(new CompletedState());
        }
    }
}
// C++ 版本
class OrderContext {
private:
    std::unique_ptr<OrderState> currentState;

public:
    OrderContext() : currentState(std::make_unique<UnpaidState>()) {}

    void setState(std::unique_ptr<OrderState> state) {
        currentState = std::move(state);
    }

    void pay(long orderId) {
        currentState->pay(orderId);
        if (dynamic_cast<UnpaidState*>(currentState.get())) {
            setState(std::make_unique<PaidState>());
        }
    }

    void cancel(long orderId) {
        currentState->cancel(orderId);
    }

    void ship(long orderId) {
        currentState->ship(orderId);
        if (dynamic_cast<PaidState*>(currentState.get())) {
            setState(std::make_unique<ShippedState>());
        }
    }

    void confirm(long orderId) {
        currentState->confirm(orderId);
        if (dynamic_cast<ShippedState*>(currentState.get())) {
            setState(std::make_unique<CompletedState>());
        }
    }
};

订单状态流转实战

我记得有一次做电商项目,订单状态有6种:待支付、已支付、已发货、已确认、已完成、已取消。每个状态之间的流转规则都不一样。

你想想看,如果用if-else写,每个方法里都得判断当前状态,然后决定能不能执行。代码会变成一坨浆糊。

用状态模式之后,每个状态类只关心自己的逻辑。新增一个状态,比如「退款中」,只需要加一个新类,不用改任何现有代码。这就是开闭原则的体现。

核心思想:状态模式把每个状态的行为封装成一个独立的类,状态切换就是换一个对象。这样代码清晰,扩展方便。

电梯状态切换

电梯的状态切换也是个经典例子。电梯有开门、关门、运行、停止、故障等状态。每个状态下能做的操作不一样。

// Java 版本 - 电梯状态接口
public interface ElevatorState {
    void openDoor();
    void closeDoor();
    void moveUp();
    void moveDown();
    void stop();
    void handleEmergency();
}
// C++ 版本
class ElevatorState {
public:
    virtual ~ElevatorState() = default;
    virtual void openDoor() = 0;
    virtual void closeDoor() = 0;
    virtual void moveUp() = 0;
    virtual void moveDown() = 0;
    virtual void stop() = 0;
    virtual void handleEmergency() = 0;
};

比如「开门状态」下,你不能移动电梯,只能关门。而「运行状态」下,你不能开门,只能停止或继续移动。

个人经验:我在做电梯控制系统时,一开始把状态切换逻辑写在了上下文里,结果上下文越来越臃肿。后来我把状态切换逻辑放到了具体状态类中,每个状态类在完成操作后,告诉上下文应该切换到哪个状态。这样上下文就变得很薄,只负责委托和状态持有。

状态模式 vs 策略模式

很多人容易把状态模式和策略模式搞混。我简单说下区别:

对比维度 状态模式 策略模式
核心目的 对象行为随状态变化 算法可替换
状态切换 状态之间可以自动切换 策略由客户端指定,一般不切换
状态数量 有限且已知 可无限扩展
典型场景 订单、电梯、工作流 排序算法、支付方式

说白了,状态模式关注的是「状态流转」,策略模式关注的是「算法选择」。一个对象的状态会变,但策略一般不会在运行时自动切换。

状态模式的优缺点

  • 优点:
    • 消除了庞大的条件分支语句
    • 每个状态独立成类,符合单一职责
    • 新增状态不影响现有代码,符合开闭原则
    • 状态流转逻辑清晰,容易维护
  • 缺点:
    • 状态多了会导致类数量膨胀
    • 状态切换逻辑分散在各个状态类中,可能不好追踪
    • 如果状态很少且不常变,用状态模式反而过度设计

避坑指南:我曾经在一个项目中,把状态模式用在了只有3个状态的场景。结果代码反而比if-else更复杂。后来我反思,状态模式适合状态多、流转规则复杂的场景。如果状态少于4个,且流转简单,用if-else或者枚举反而更直接。

状态模式的核心流程图

状态模式核心结构图 上下文 持有当前状态引用 状态接口 定义行为契约 具体状态A 实现状态接口 处理A状态下的行为 具体状态B 实现状态接口 处理B状态下的行为 具体状态C 实现状态接口 处理C状态下的行为 状态切换 状态切换 上下文委托请求给当前状态对象,状态对象决定是否切换状态

什么时候用状态模式

我个人总结了几条经验:

  • 对象的行为依赖于它的状态,并且状态会变化
  • 状态数量不少于4个,且流转规则复杂
  • 每个状态下的行为差异很大
  • 你希望新增状态时不影响现有代码

如果满足以上条件,状态模式就是你的好选择。如果不满足,用枚举+简单判断可能更合适。

小技巧:状态模式可以和工厂模式结合使用。比如在上下文中,用一个状态工厂来创建状态对象,这样状态切换的逻辑就更清晰了。我在一个工作流引擎中就是这么做的,效果很好。

好了,状态模式就讲到这里。记住一句话:状态模式让对象的行为随状态而变,而不是用if-else硬编码。下次你遇到状态流转的场景,不妨试试这个模式。


公众号:蓝海资料掘金营,微信deep3321