22、状态模式:模式动机与定义、角色与结构、C++代码实现、状态机应用
模式动机:为什么我们需要状态模式?
先聊个我自己的经历。几年前我接手过一个通信协议解析模块,里面有个对象会根据收到的报文类型,在「空闲」、「同步」、「数据接收」、「校验」这几个状态之间跳转。最初的实现方式很简单——一个枚举变量加一个巨大的 switch-case。
嗯,刚开始确实爽。写起来快,逻辑也直观。但三个月后需求变了,要新增一个「重传」状态。我打开那个文件,看到 switch 里密密麻麻的分支,头皮都麻了。改一个状态,得去翻所有 case 有没有遗漏。更可怕的是,某个 case 里忘了 break,直接穿透到下一个状态,线上出了个隐蔽的 bug。
你想想看,这种场景其实很常见。一个对象的行为,完全取决于它内部的状态。状态一多,条件分支就爆炸。每次新增状态,都要动既有代码,改一处可能影响全局。这就是状态模式要解决的问题——把状态相关的行为抽出来,封装成独立的状态类,让状态切换变成对象替换。
核心动机:允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
模式定义与角色结构
状态模式的定义其实很简洁:将每个状态封装成一个类,状态之间的转换由状态类自身或上下文类来管理。说白了,就是把原来散落在 switch-case 里的逻辑,搬到各自的状态类里去。
来看一下它的角色结构:
- Context(上下文):持有当前状态对象的引用。对外暴露的接口,实际上委托给当前状态对象去执行。
- State(抽象状态):定义所有具体状态的公共接口。通常是一个抽象类或接口。
- ConcreteState(具体状态):实现 State 接口,封装了该状态下的具体行为。同时负责决定何时切换到下一个状态。
我习惯把 Context 想象成一个「遥控器」,State 就是「当前按下的模式键」。按了空调模式,遥控器就表现出空调的行为;按了电视模式,就表现出电视的行为。对象还是那个对象,但行为变了。
C++ 代码实现
直接上代码。我们实现一个简单的 TCP 连接状态机:有 Established、Listening、Closed 三个状态。每个状态对「打开连接」、「关闭连接」、「监听」这三个操作有不同的响应。
// 前向声明
class TCPConnection;
// 抽象状态类
class TCPState {
public:
virtual ~TCPState() = default;
virtual void Open(TCPConnection* conn) = 0;
virtual void Close(TCPConnection* conn) = 0;
virtual void Listen(TCPConnection* conn) = 0;
};
// 上下文类
class TCPConnection {
public:
TCPConnection() : state_(new ClosedState()) {}
void SetState(TCPState* s) {
delete state_;
state_ = s;
}
void Open() { state_->Open(this); }
void Close() { state_->Close(this); }
void Listen() { state_->Listen(this); }
private:
TCPState* state_;
};
// 具体状态:Closed
class ClosedState : public TCPState {
public:
void Open(TCPConnection* conn) override {
std::cout << "从 Closed 切换到 Established\n";
conn->SetState(new EstablishedState());
}
void Close(TCPConnection* conn) override {
std::cout << "已经是 Closed 状态,无需操作\n";
}
void Listen(TCPConnection* conn) override {
std::cout << "从 Closed 切换到 Listening\n";
conn->SetState(new ListeningState());
}
};
// 具体状态:Established
class EstablishedState : public TCPState {
public:
void Open(TCPConnection* conn) override {
std::cout << "连接已建立,不能重复 Open\n";
}
void Close(TCPConnection* conn) override {
std::cout << "从 Established 切换到 Closed\n";
conn->SetState(new ClosedState());
}
void Listen(TCPConnection* conn) override {
std::cout << "从 Established 切换到 Listening\n";
conn->SetState(new ListeningState());
}
};
// 具体状态:Listening
class ListeningState : public TCPState {
public:
void Open(TCPConnection* conn) override {
std::cout << "从 Listening 切换到 Established\n";
conn->SetState(new EstablishedState());
}
void Close(TCPConnection* conn) override {
std::cout << "从 Listening 切换到 Closed\n";
conn->SetState(new ClosedState());
}
void Listen(TCPConnection* conn) override {
std::cout << "已经在 Listening 状态\n";
}
};
个人经验:我习惯把状态切换的逻辑放在具体状态类的 Handle 方法里,而不是放在 Context 中。这样新增状态时,只需要新增一个类,改一下相关状态的切换逻辑,Context 完全不用动。符合开闭原则。
状态机应用:从理论到实战
状态模式最常见的应用场景就是有限状态机(FSM)。我在项目中用状态模式写过协议栈、游戏角色 AI、工作流引擎。这里分享一个我踩过的坑。
避坑指南:我曾经在一个状态机里,让每个状态类在 Handle 方法中直接 delete 自己,然后 new 出下一个状态。结果在状态切换时,当前对象已经被销毁了,后续代码还在访问它的成员变量——直接崩溃。后来我改成在 Context 里统一管理状态对象的生命周期,状态类只负责返回「下一个状态类型」的标识,由 Context 来执行切换和销毁。
来看一个更实用的状态机框架。假设我们要实现一个电梯控制系统,有「静止」、「运行中」、「开门」、「故障」四个状态。每个状态对「按楼层」、「关门」、「开门」、「急停」等事件有不同的响应。
| 当前状态 | 按楼层 | 关门 | 开门 | 急停 |
|---|---|---|---|---|
| 静止 | → 运行中 | 无变化 | → 开门 | → 故障 |
| 运行中 | 排队等待 | 无变化 | 不允许 | → 故障 |
| 开门 | 排队等待 | → 静止 | 无变化 | → 故障 |
| 故障 | 不允许 | 不允许 | 不允许 | 无变化 |
你看,用状态表来设计,逻辑一目了然。每个格子对应一个状态类里的一个方法实现。新增一个「检修」状态,只需要新增一个类,填好这张表里的对应行为就行。原有的代码一行都不用改。
核心要点:状态模式不是银弹。如果状态只有两三个,switch-case 反而更简洁。我个人的判断标准是:当状态数量超过 4 个,或者状态转换规则经常变化时,果断上状态模式。
最后说一个细节。状态模式里的状态对象,很多时候是无状态的——它本身不保存数据,只封装行为。这种情况下,多个 Context 可以共享同一个状态实例,用单例模式实现具体状态类,能省不少内存。嗯,这个技巧我在一个嵌入式项目里用过,效果很好。
状态模式的核心思想,说白了就是把变化封装起来。状态会变,行为会变,那就让每个状态自己管好自己的事。你想想看,这其实跟现实世界很像——人在不同状态下,行为就是不一样。代码也应该是这样。