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

状态模式,说白了就是让一个对象在内部状态改变时,它的行为也跟着变。你想想看,这就像一个人——高兴的时候哼着歌,生气的时候摔东西,疲惫的时候躺平。同一个对象,状态不同,行为完全不同。

我在项目中遇到过不少这样的场景。比如一个网络连接对象,它可能处于已连接正在重连已断开等状态。每个状态下,调用send()方法的行为都不一样。如果不用状态模式,你大概率会写一堆if-else或者switch-case,代码又臭又长,还容易漏掉某个状态分支。

核心思想:将每个状态的行为封装到独立的类中,让状态对象自己去决定该做什么。主对象(上下文)只负责委托给当前状态对象。

模式动机与定义

为什么需要状态模式?我个人的习惯是,当看到一个对象的行为随着内部状态变化而剧烈变化,而且状态数量可能增加时,就该考虑状态模式了。

举个例子,一个自动售货机:

  • 待机状态:等待投币,投币后进入“已投币”状态
  • 已投币状态:可以选择商品,选择后进入“出货中”状态
  • 出货中状态:出货,出货完成后回到“待机状态”

每个状态下,用户的操作(投币、选择、取消)都有不同的响应。用状态模式,每个状态就是一个类,清晰又容易扩展。

定义:状态模式允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

我的经验:状态模式特别适合那些状态转换规则明确、状态数量有限的场景。我曾经在一个通信协议栈里用了状态模式,把几十个状态管理得井井有条。后来加新状态时,只需要新增一个类,改改转换规则,完全不影响现有代码。

角色与结构

状态模式涉及三个核心角色:

角色 名称 职责
上下文 Context 维护一个当前状态对象的引用,对外提供统一的接口
抽象状态 State 定义所有状态共有的接口,声明状态相关行为
具体状态 ConcreteState 实现具体状态下的行为,并决定何时转换到下一个状态

结构图如下:

Context - state: State* + request() State + handle(Context*) ConcreteStateA + handle(Context*) ConcreteStateB + handle(Context*) ConcreteStateC + handle(Context*) 持有 派生

你看这个结构,Context 里只存了一个 State 指针。每次调用 request(),它就委托给当前状态对象的 handle() 方法。状态对象内部可以改变 Context 的状态——说白了,就是把自己换成另一个状态对象。

C++ 代码实现

直接上代码。我习惯用 C++11 及以上标准,智能指针管理状态生命周期,避免裸指针的麻烦。

// 前向声明
class Context;

// 抽象状态类
class State {
public:
    virtual ~State() = default;
    virtual void handle(Context* ctx) = 0;
};

// 上下文类
class Context {
private:
    std::unique_ptr<State> currentState;
public:
    explicit Context(std::unique_ptr<State> state)
        : currentState(std::move(state)) {}

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

    void request() {
        if (currentState) {
            currentState->handle(this);
        }
    }
};

// 具体状态A
class ConcreteStateA : public State {
public:
    void handle(Context* ctx) override {
        std::cout << "状态A:处理请求,切换到状态B\n";
        ctx->setState(std::make_unique<ConcreteStateB>());
    }
};

// 具体状态B
class ConcreteStateB : public State {
public:
    void handle(Context* ctx) override {
        std::cout << "状态B:处理请求,切换到状态A\n";
        ctx->setState(std::make_unique<ConcreteStateA>());
    }
};

注意:状态对象内部调用 ctx->setState() 时,会销毁当前状态对象自身。这在 C++ 里是安全的——因为 handle() 执行完后,当前状态对象才被析构。但如果你在 handle() 之后还访问了当前对象的成员,那就出事了。我曾经踩过这个坑,后来养成了习惯:状态转换代码永远放在 handle() 的最后一行。

状态机示例:自动售货机

来个实际点的例子。自动售货机有三个状态:待机已投币出货中。每个状态下,用户的操作不同。

// 前向声明
class VendingMachine;

// 抽象状态
class MachineState {
public:
    virtual ~MachineState() = default;
    virtual void insertCoin(VendingMachine* vm) = 0;
    virtual void selectProduct(VendingMachine* vm) = 0;
    virtual void dispense(VendingMachine* vm) = 0;
};

// 自动售货机(上下文)
class VendingMachine {
private:
    std::unique_ptr<MachineState> state;
    int productCount = 5;  // 剩余商品数量
public:
    VendingMachine() 
        : state(std::make_unique<IdleState>()) {}

    void setState(std::unique_ptr<MachineState> s) {
        state = std::move(s);
    }

    void insertCoin() { state->insertCoin(this); }
    void selectProduct() { state->selectProduct(this); }
    void dispense() { state->dispense(this); }

    int getProductCount() const { return productCount; }
    void reduceProduct() { if (productCount > 0) productCount--; }
};

// 待机状态
class IdleState : public MachineState {
public:
    void insertCoin(VendingMachine* vm) override {
        std::cout << "投币成功,进入已投币状态\n";
        vm->setState(std::make_unique<HasCoinState>());
    }
    void selectProduct(VendingMachine*) override {
        std::cout << "请先投币\n";
    }
    void dispense(VendingMachine*) override {
        std::cout << "请先投币并选择商品\n";
    }
};

// 已投币状态
class HasCoinState : public MachineState {
public:
    void insertCoin(VendingMachine*) override {
        std::cout << "已经投过币了,请选择商品\n";
    }
    void selectProduct(VendingMachine* vm) override {
        if (vm->getProductCount() > 0) {
            std::cout << "选择商品成功,进入出货中状态\n";
            vm->setState(std::make_unique<DispensingState>());
        } else {
            std::cout << "商品已售罄,退回硬币\n";
            vm->setState(std::make_unique<IdleState>());
        }
    }
    void dispense(VendingMachine*) override {
        std::cout << "请先选择商品\n";
    }
};

// 出货中状态
class DispensingState : public MachineState {
public:
    void insertCoin(VendingMachine*) override {
        std::cout << "正在出货,请稍候\n";
    }
    void selectProduct(VendingMachine*) override {
        std::cout << "正在出货,请稍候\n";
    }
    void dispense(VendingMachine* vm) override {
        vm->reduceProduct();
        std::cout << "出货完成,回到待机状态\n";
        vm->setState(std::make_unique<IdleState>());
    }
};

使用起来也很直观:

int main() {
    VendingMachine vm;
    vm.insertCoin();      // 投币成功,进入已投币状态
    vm.selectProduct();   // 选择商品成功,进入出货中状态
    vm.dispense();        // 出货完成,回到待机状态
    vm.dispense();        // 请先投币并选择商品
    return 0;
}

避坑指南:我曾经在一个项目中,状态转换逻辑散落在各个状态类的 handle 方法里,结果状态图一复杂,转换关系就乱成一团。后来我学乖了——把状态转换表单独抽出来,或者用状态图工具先画清楚再编码。状态模式虽然灵活,但状态转换的维护成本不低,建议配合状态图一起使用。

状态模式的核心价值,就是把变化的行为从上下文里剥离出来,让每个状态自己管自己。你想想看,如果以后要加一个“退款中”状态,只需要新增一个类,改改转换规则,完全不用动 VendingMachine 的代码。这就是开闭原则的体现——对扩展开放,对修改关闭。

嗯,状态模式就聊到这儿。记住一点:状态多、行为杂、转换规则明确——这三个条件满足时,状态模式就是你的好帮手。


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