①今日主题
状态模式(State Pattern)是 GoF 二十三种设计模式中的行为型模式。它把"对象在不同状态下表现出的不同行为"封装成一组独立的状态类,让对象在内部状态发生改变时自动切换自己的行为——对客户端而言,对象仿佛"换了一个自己":同一个 play() 调用,在"播放中"和"已暂停"两个状态下产生完全不同的效果,而调用方根本不需要关心对象当前处于哪个状态。
一句话定义:把状态变成对象,让行为跟着状态走;状态切换由状态对象自己决定,新增状态只加类、不改旧代码。
②为什么需要这个模式
先看一段"没有模式"的代码。假设我们要写一个媒体播放器,它有停止、播放、暂停三种状态,每种状态下 play / pause / stop 三个操作的行为都不一样。新手最常见的写法是"枚举 + if-else":
// ---- 坏味道:枚举 + if-else 硬编码状态机 ----
#include <iostream>
enum class PlayState { Stopped, Playing, Paused };
class BadPlayer {
public:
void play() {
if (state_ == PlayState::Stopped) {
std::cout << "开始播放\n";
state_ = PlayState::Playing;
} else if (state_ == PlayState::Playing) {
std::cout << "已在播放\n";
} else { // Paused
std::cout << "继续播放\n";
state_ = PlayState::Playing;
}
}
void pause() {
if (state_ == PlayState::Playing) {
std::cout << "已暂停\n";
state_ = PlayState::Paused;
} else {
std::cout << "无法暂停\n";
}
}
void stop() {
if (state_ != PlayState::Stopped) {
std::cout << "已停止\n";
state_ = PlayState::Stopped;
}
}
private:
PlayState state_ = PlayState::Stopped; // 当前状态
};
这段代码表面能跑,但有四个越滚越大的问题:
- 条件判断膨胀:每增加一个操作(比如"快进"),就要再写一整条 if-else 链去枚举所有状态;每增加一个状态(比如"缓冲中"),所有操作方法都要改一遍。需要维护的分支数 = 状态数 × 操作数,呈乘法增长。
- 违背开闭原则(OCP):新增状态必须回头修改已有的
play / pause / stop三个方法——对扩展开放的同时,对修改也开放了。 - 违背单一职责原则(SRP):一个类同时承担了"业务动作"(播放、暂停)和"状态流转"(什么时候切到哪个状态)两类职责,任何一个状态规则变了,都要动这个类。
- 转移逻辑散落、易错:整个状态机的转移表被打散在各个 if-else 分支里,很难一眼看出"从哪个状态能到哪个状态"。漏写一个分支不会编译报错,只会在某个特定状态下触发诡异的运行时行为——这是状态机 bug 最可怕的地方。
核心诉求:把"状态判断"从业务方法中剥离出去,让每个状态对自己的行为负责,让"换状态"成为一件低成本、无副作用的操作。这正是状态模式要解决的问题。
③核心思想
状态模式的核心转变是:把"状态"从"一个枚举值"升级为"一个对象"。每个状态对象都是一名"专家"——它只知道自己这种状态下每个操作该做什么、做完之后应该转移到哪个状态。上下文对象(Context)不再自己做任何状态判断,它只保存一个"当前状态对象"的指针,把客户端的每个请求原封不动地委托给这个状态对象;状态对象执行完自己的行为后,如果认为需要换状态,就调用 Context 的切换接口把自己换掉。
整个过程可以用一句话概括:请求 → 委托 → 状态对象处理 → (可能)自我替换。客户端从头到尾只看到"同一个对象在调用同一个方法",但这个对象的行为却在不断变化。
生活类比一:水的三态。同样是 H₂O 分子,0°C 以下是固态(坚硬),0~100°C 是液态(流动),100°C 以上是气态(扩散)。"水"本身没有变,变的只是它处于哪个状态,而每个状态下的表现天差地别。状态模式里的 Context 就是"水分子",三个具体状态就是固、液、气。
生活类比二:交通信号灯。红灯知道自己在 30 秒后变成绿灯,绿灯知道自己在 25 秒后变成黄灯,黄灯知道自己在 5 秒后变成红灯。每个灯都只关心"自己这个状态下该亮多久、下一个是谁",整个交通系统不需要一个全局大脑来调度——状态之间的转移规则被打散到每个状态自己身上,谁也不会越权。
游戏场景类比:游戏角色有"站立 / 跑动 / 跳跃 / 受击 / 死亡"等状态。站立时可以响应移动键,跳跃中不能二段跳,死亡后任何输入都无效。如果用 if-else 写,每个输入处理函数都要判断"我现在是什么状态";用状态模式,每个状态类自己实现"这个状态下按跳跃键会怎样",代码结构与游戏设计文档一一对应。
④UML / 角色关系
状态模式包含三个角色:
| 角色 | 本例类名 | 职责 |
|---|---|---|
| Context(上下文) | MediaPlayer | 持有当前状态对象的指针;把客户端的请求委托给当前状态;对外提供 setState() 供状态对象切换自己 |
| State(抽象状态) | State | 定义每种状态下所有操作的接口(play / pause / stop),不关心具体实现 |
| ConcreteState(具体状态) | StoppedState / PlayingState / PausedState | 实现本状态下的具体行为;在合适的时机调用 Context::setState() 触发状态转移 |
对象结构:Context 以组合方式持有一个 State*(现代 C++ 用 std::unique_ptr<State> 管理所有权);State 是抽象基类;三个 ConcreteState 继承并实现它。状态转移由 ConcreteState 发起——它在自己的行为方法中调用 Context::setState(),把内部指针指向另一个 ConcreteState。
以媒体播放器为例,状态转移图如下(箭头上的标注是触发转移的操作):
Stopped --play()--> Playing
Playing --pause()--> Paused
Paused --play()--> Playing
Playing --stop()--> Stopped
Paused --stop()--> Stopped
注意这张图就是后面 ⑤ 中代码的结构图:每个箭头对应一个具体状态类方法体里的一行 setState(...) 调用。设计图与代码一一对应,是状态模式在工程上最大的红利。
⑤最小 C++ 示例
下面用 C++17 完整实现上面的媒体播放器。注意三个具体状态之间存在循环引用(停止要转到播放、暂停要转到停止……),所以采用"类内只声明、类外定义方法体"的标准写法,保证每个状态类在方法体中使用 std::make_unique 时,目标状态类已经是完整类型:
// ============ state_minimal.cpp —— C++17 完整可编译 ============
#include <iostream>
#include <memory>
#include <string>
class MediaPlayer; // 前置声明:状态类的接口方法以 Context 为参数
// ---------- 抽象状态 State ----------
class State {
public:
virtual ~State() = default;
virtual void play(MediaPlayer &player) = 0; // 处理"播放"请求
virtual void pause(MediaPlayer &player) = 0; // 处理"暂停"请求
virtual void stop(MediaPlayer &player) = 0; // 处理"停止"请求
virtual std::string name() const = 0; // 状态名(便于观察)
};
// ---------- 具体状态前置声明(存在循环依赖,方法体放到类外) ----------
class StoppedState;
class PlayingState;
class PausedState;
// ---------- 上下文 Context:持有当前状态对象,请求一律委托 ----------
class MediaPlayer {
public:
explicit MediaPlayer(std::unique_ptr<State> initial)
: state_(std::move(initial)) {}
// 状态切换入口:由具体状态对象在"该转移时"调用
void setState(std::unique_ptr<State> next) {
state_ = std::move(next); // RAII:旧状态对象自动析构,无泄漏
std::cout << " [状态切换 -> " << state_->name() << "]\n";
}
// 对外接口:不判断任何状态,直接委托给当前状态对象
void play() { state_->play(*this); }
void pause() { state_->pause(*this); }
void stop() { state_->stop(*this); }
private:
std::unique_ptr<State> state_; // 当前状态(所有权归 Context)
};
// ---------- 具体状态 1:停止 ----------
class StoppedState final : public State {
public:
void play(MediaPlayer &player) override;
void pause(MediaPlayer &player) override;
void stop(MediaPlayer &player) override;
std::string name() const override { return "停止"; }
};
// ---------- 具体状态 2:播放中 ----------
class PlayingState final : public State {
public:
void play(MediaPlayer &player) override;
void pause(MediaPlayer &player) override;
void stop(MediaPlayer &player) override;
std::string name() const override { return "播放中"; }
};
// ---------- 具体状态 3:已暂停 ----------
class PausedState final : public State {
public:
void play(MediaPlayer &player) override;
void pause(MediaPlayer &player) override;
void stop(MediaPlayer &player) override;
std::string name() const override { return "已暂停"; }
};
// ---------- 方法体:此时三个具体状态均已完整定义,可以互相 new ----------
void StoppedState::play(MediaPlayer &player) {
std::cout << "开始播放\n";
player.setState(std::make_unique<PlayingState>()); // 停止 --play()--> 播放
}
void StoppedState::pause(MediaPlayer &player) {
std::cout << "当前已停止,无法暂停\n"; // 非法操作:忽略并提示
}
void StoppedState::stop(MediaPlayer &player) {
std::cout << "已经处于停止状态\n"; // 非法操作:忽略并提示
}
void PlayingState::play(MediaPlayer &player) {
std::cout << "正在播放中(忽略)\n"; // 非法操作:忽略并提示
}
void PlayingState::pause(MediaPlayer &player) {
std::cout << "已暂停\n";
player.setState(std::make_unique<PausedState>()); // 播放 --pause()--> 暂停
}
void PlayingState::stop(MediaPlayer &player) {
std::cout << "已停止\n";
player.setState(std::make_unique<StoppedState>()); // 播放 --stop()--> 停止
}
void PausedState::play(MediaPlayer &player) {
std::cout << "继续播放\n";
player.setState(std::make_unique<PlayingState>()); // 暂停 --play()--> 播放
}
void PausedState::pause(MediaPlayer &player) {
std::cout << "已经处于暂停状态\n"; // 非法操作:忽略并提示
}
void PausedState::stop(MediaPlayer &player) {
std::cout << "已停止\n";
player.setState(std::make_unique<StoppedState>()); // 暂停 --stop()--> 停止
}
// ---------- 客户端 ----------
int main() {
MediaPlayer player(std::make_unique<StoppedState>()); // 初始状态:停止
player.play(); // 停止 -> 播放
player.play(); // 已在播放,忽略
player.pause(); // 播放 -> 暂停
player.play(); // 暂停 -> 播放(继续)
player.stop(); // 播放 -> 停止
player.pause(); // 停止状态下的非法操作
return 0;
}
程序输出:
开始播放
[状态切换 -> 播放中]
正在播放中(忽略)
已暂停
[状态切换 -> 已暂停]
继续播放
[状态切换 -> 播放中]
已停止
[状态切换 -> 停止]
当前已停止,无法暂停
编译方式:g++ -std=c++17 state_minimal.cpp -o state_minimal && ./state_minimal。可以看到:客户端 main() 里没有任何一个 if 判断,所有"该不该做、做完了去哪"的规则都封装在三个状态类内部;非法操作(停止时暂停、播放中播放)被对应的状态对象静默忽略,从结构上杜绝了漏判分支。
⑥Qt 实战示例
好消息:这个模式 Qt 已经替你完整实现了一遍,而且做成了基础设施——就是 Qt 状态机框架(State Machine Framework):QStateMachine(状态机容器,即 Context)、QState(状态)、QFinalState(终态)、QHistoryState(历史状态)、QAbstractTransition(转移)。你不需要手写状态类,只需要声明状态、声明转移、把转移和信号绑定起来。
下面的例子是一个三态订单演示:待支付 → 已支付 → 已完成,每次点击按钮推进一个状态,标签文本由状态机自动更新。assignProperty 的作用是"进入某状态时,自动把对象的某个属性设置为指定值":
// ============ main.cpp —— Qt6 + C++17 状态机演示 ============
#include <QApplication>
#include <QStateMachine>
#include <QState>
#include <QLabel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
// ---- 界面:一个标签 + 一个按钮 ----
QWidget window;
auto *layout = new QVBoxLayout(&window);
auto *label = new QLabel("待支付"); // 文本由状态机自动更新
auto *button = new QPushButton("推进状态");
layout->addWidget(label);
layout->addWidget(button);
// ---- 状态机:声明三个状态 ----
QStateMachine machine; // Context 角色
auto *pending = new QState(); // 待支付
auto *paid = new QState(); // 已支付
auto *done = new QState(); // 已完成
// 进入某状态时,自动设置 label 的 text 属性
pending->assignProperty(label, "text", "待支付");
paid->assignProperty(label, "text", "已支付");
done->assignProperty(label, "text", "已完成");
// ---- 转移:按钮 clicked 信号驱动 ----
pending->addTransition(button, &QPushButton::clicked, paid);
paid->addTransition(button, &QPushButton::clicked, done);
done->addTransition(button, &QPushButton::clicked, pending); // 循环演示
machine.addState(pending);
machine.addState(paid);
machine.addState(done);
machine.setInitialState(pending);
machine.start(); // 启动状态机
window.show();
return app.exec();
}
# ============ CMakeLists.txt ============
cmake_minimum_required(VERSION 3.16)
project(qt-state-demo VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Widgets)
qt_standard_project_setup()
qt_add_executable(qt-state-demo main.cpp)
target_link_libraries(qt-state-demo PRIVATE Qt6::Widgets)
运行效果:编译运行后,窗口显示一个标签和一个按钮;每次点击按钮,状态机沿"待支付 → 已支付 → 已完成 → 待支付……"循环转移,标签文本随之自动更新。这个例子展示了 Qt 状态机的三个核心能力:信号驱动转移(addTransition(sender, signal, target),按钮点击信号直接触发转移)、属性绑定(assignProperty,进入状态自动改属性)、自动管理(状态机负责进入/退出状态、触发 onEntry / onExit 回调)。
工程启示:当框架已经提供成熟的状态机基础设施时,不要自己重复造轮子。手写 GoF 状态模式适合库内部、无框架依赖的场景;在 Qt 项目里,优先考虑 QStateMachine——它把"状态转移"做成了声明式配置,比手写状态类更直观、更不易错。
⑦代码执行流程
以 ⑤ 的最小示例为例,程序从启动到结束的完整执行流程:
- 初始化:main() 创建 MediaPlayer,传入
std::make_unique<StoppedState>(),内部指针state_指向"停止"状态对象。 - 第一次 play():Context 不做任何判断,直接把请求委托给当前状态对象
StoppedState::play()。 - 状态对象处理:
StoppedState::play()打印"开始播放",然后调用player.setState(std::make_unique<PlayingState>())——由状态对象自己决定转移。 - 状态切换:
setState()用std::move把新状态对象装入unique_ptr,旧状态对象随unique_ptr析构自动释放(RAII,无内存泄漏),随后打印"状态切换 -> 播放中"。 - 第二次 play():请求委托给
PlayingState::play(),它打印"正在播放中(忽略)",不发生转移——同一方法、不同状态、不同行为。 - pause():委托给
PlayingState::pause(),打印"已暂停"并转移到 PausedState。 - 后续调用:每次请求都重复"委托 → 状态对象处理 → 可能转移"这一循环,直到 main() 返回、MediaPlayer 析构,状态对象随成员
unique_ptr自动释放。全程客户端代码零分支、零手动内存管理。
这个流程的精髓在于第 2、5 步的对比:客户端代码一模一样,行为却完全不同——因为行为的所有权已经从"调用方"转移到了"状态对象"手里。