设计模式之状态模式

简单来说,**状态模式(State Pattern)**就是为了解决代码中那堆令人头疼的 if-elseswitch-case。当一个对象的行为取决于它的"状态"时,我们干脆把每种状态都封装成一个独立的类。

场景设定:自动贩卖机

想象一个简单的自动贩卖机,它有两个状态:

  1. NoMoney(待投币)

  2. HasMoney(已投币)

动作有两个:投币退币

1. 不使用模式:基于 switch-case 的硬编码

这种方式下,所有的逻辑都堆在一个类里,每增加一个动作或状态,都要修改所有现有的函数。

复制代码
#include <iostream>

enum State { NO_MONEY, HAS_MONEY, SOLD_OUT };

class VendingMachine {
private:
    State state = NO_MONEY;

public:
    void insertMoney() {
        switch (state) {
            case NO_MONEY:
                std::cout << "投币成功!" << std::endl;
                state = HAS_MONEY;
                break;
            case HAS_MONEY:
                std::cout << "已有硬币,请勿重复投币。" << std::endl;
                break;
            case SOLD_OUT:
                std::cout << "已售罄,投币失败。" << std::endl;
                break;
        }
    }

    void ejectMoney() {
        switch (state) {
            case HAS_MONEY:
                std::cout << "退币成功。" << std::endl;
                state = NO_MONEY;
                break;
            case NO_MONEY:
                std::cout << "没钱可退。" << std::endl;
                break;
            case SOLD_OUT:
                std::cout << "抱歉,没投币也没货了。" << std::endl;
                break;
        }
    }
};

int main() {
    VendingMachine machine;
    machine.insertMoney();
    machine.ejectMoney();
    return 0;
}

2. 使用状态模式:面向对象的状态迁移

这种方式将"状态"抽离成类。VendingMachine 只负责转发请求,具体的逻辑由当前的状态对象自己处理。

复制代码
#include <iostream>
#include <memory>

class VendingMachine;

// 1. 抽象状态类
class State {
public:
    virtual ~State() = default;
    virtual void insertMoney(VendingMachine& machine) = 0;
    virtual void ejectMoney(VendingMachine& machine) = 0;
};

// 2. 环境类(贩卖机)
class VendingMachine {
private:
    std::unique_ptr<State> currentState;

public:
    VendingMachine(std::unique_ptr<State> initialState) 
        : currentState(std::move(initialState)) {}

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

    void insertMoney() { currentState->insertMoney(*this); }
    void ejectMoney() { currentState->ejectMoney(*this); }
};

// 3. 具体状态:已投币
class HasMoneyState : public State {
public:
    void insertMoney(VendingMachine& machine) override {
        std::cout << "错误:已有硬币。" << std::endl;
    }
    void ejectMoney(VendingMachine& machine) override; 
};

// 4. 具体状态:待投币
class NoMoneyState : public State {
public:
    void insertMoney(VendingMachine& machine) override {
        std::cout << "投币成功!" << std::endl;
        // 状态切换逻辑:交给具体类处理
        machine.setState(std::make_unique<HasMoneyState>());
    }
    void ejectMoney(VendingMachine& machine) override {
        std::cout << "错误:没钱可退。" << std::endl;
    }
};

// 实现 HasMoneyState 的延迟定义
void HasMoneyState::ejectMoney(VendingMachine& machine) {
    std::cout << "退币成功!" << std::endl;
    machine.setState(std::make_unique<NoMoneyState>());
}

int main() {
    // 初始状态为"待投币"
    VendingMachine machine(std::make_unique<NoMoneyState>());
    
    machine.insertMoney(); // 投币
    machine.ejectMoney();  // 退币
    
    return 0;
}

对比总结

特性 硬编码 (switch-case) 状态模式 (State Pattern)
逻辑分布 逻辑集中在一个类,极其臃肿。 逻辑分散在具体状态类,符合单一职责。
扩展性 差。增加状态需要改动所有函数源码。 优。增加状态只需新建一个类。
可读性 分支越多,代码越像"屎山"。 逻辑清晰,像是在阅读状态转移图。
适用场景 状态极少(2-3个)且永远不会变。 状态多、逻辑复杂、经常需要扩展。

假设老板突然加了一个新需求:"增加一个'中奖'功能。投币后有 10% 的概率中奖,中奖后直接出货,不需要按按钮。"


1. 不使用模式:在 switch-case 里"动手术"

你必须深入到原有的每一个动作函数里,小心翼翼地插入 if 判断。

复制代码
void VendingMachine::insertMoney() {
    switch (state) {
        case NO_MONEY:
            std::cout << "投币成功!" << std::endl;
            // --- 新增需求逻辑:硬塞进去 ---
            if (rand() % 10 == 0) { 
                std::cout << "恭喜中奖!自动出货。" << std::endl;
                state = NO_MONEY; // 中奖完直接回到初始
            } else {
                state = HAS_MONEY;
            }
            // --------------------------
            break;
        case HAS_MONEY:
            // ... 原有逻辑 ...
            break;
    }
}

风险: 如果你的代码有 1000 行,你得满世界找哪里需要加这个 if。一旦漏掉一个地方,逻辑就出 Bug 了(比如中奖了结果还能退币)。


2. 使用状态模式:新增一个"状态类"

完全不需要修改 VendingMachine 类,也不需要修改 NoMoneyState。你只需要像插拔 U 盘一样,增加一个新的逻辑块。

第一步:新建一个状态类(或者修改现有的特定状态) 我们只需要在 NoMoneyState(投币那个瞬间)做文章:

复制代码
void NoMoneyState::doInsert(VendingMachine* vm) {
    std::cout << "投币成功!" << std::endl;
    
    if (rand() % 10 == 0) {
        std::cout << "恭喜中奖!" << std::endl;
        // 逻辑直接跳过"已投币",执行出货逻辑,最后回到自己
        vm->changeState(new NoMoneyState()); 
    } else {
        vm->changeState(new HasMoneyState());
    }
}

但当你的需求变成:

  1. 投币成功 -> 2. 选择商品 -> 3. 检查库存 -> 4. 出货中 -> 5. 故障处理 -> 6. 维护模式...

这时候,状态模式 就像是把一团乱麻拆成了 一格一格的抽屉

  • 以后想改"故障处理"的逻辑?直接拉开那个抽屉改就行。

  • 其他抽屉(投币、出货)动都不用动,绝对不会被你改出 Bug。

一句话总结:

  • 硬编码 是把逻辑写在动作里(投币时判断状态)。

  • 状态模式 是把逻辑写在状态里(在没钱状态下处理投币)。

相关推荐
杨充15 小时前
10.可测试性实战设计
设计模式·开源·代码规范
杨充15 小时前
9.重构十二式的实战
设计模式·开源·代码规范
杨充15 小时前
6.设计原则的全景图
设计模式·开源·全栈
杨充15 小时前
2.面向对象的特性
设计模式
杨充15 小时前
7.SOLID原则案例汇
设计模式·开源·全栈
杨充15 小时前
8.反模式与坏味道
设计模式·开源·代码规范
杨充16 小时前
3.接口vs抽象类比较
设计模式
咖啡八杯19 小时前
文法、BNF与AST
java·设计模式·解释器模式·ast·文法
咖啡八杯1 天前
GoF设计模式——解释器模式
java·后端·spring·设计模式