状态模式:让电梯像人一样知道该做什么
状态模式,说白了就是让对象根据内部状态改变行为。听起来像废话?嗯,我当年第一次接触这个模式时也是这反应。直到我在一个物联网项目中,被一堆 if-else 折磨得欲哭无泪,才真正体会到状态模式的价值。
今天咱们就用电梯这个经典场景,把状态模式彻底讲透。你想想看,电梯的运行逻辑其实特别适合用状态模式——它无非就是开门、关门、运行、停止这几种状态,每种状态下能做什么、不能做什么,规则非常清晰。
为什么不用 if-else?
我见过不少新手写电梯逻辑,上来就是:
if (state == "开门") {
if (action == "关门") { ... }
else if (action == "运行") { // 非法操作 }
} else if (state == "运行") {
if (action == "开门") { // 非法操作 }
...
}
这种写法的问题在哪?状态和行为的耦合太紧。每增加一个状态,你都得去改所有条件分支。我在一个项目中接手过类似的代码,光电梯状态就有 7 种,动作有 10 多个,那个 if-else 嵌套看得我头皮发麻。
状态模式的核心思想是:把每个状态封装成独立的类,状态自己的行为自己管。这样新增状态时,你只需要加一个新类,完全不用动现有代码。
电梯状态模式的架构设计
先看整体结构,我画了张图帮你理解:
这个架构里,电梯上下文只负责持有当前状态对象,所有行为都委托给状态对象处理。每个状态类都实现了同一个接口,但内部逻辑完全不同。
代码实现:从接口到具体状态
先定义状态接口。我个人习惯用抽象类,因为可以放一些公共方法:
class ElevatorState {
public:
virtual ~ElevatorState() = default;
// 核心行为接口
virtual void openDoor(Elevator& elevator) = 0;
virtual void closeDoor(Elevator& elevator) = 0;
virtual void run(Elevator& elevator) = 0;
virtual void stop(Elevator& elevator) = 0;
// 获取状态名称(方便调试)
virtual std::string getStateName() const = 0;
protected:
// 状态切换辅助方法
void changeState(Elevator& elevator, ElevatorState* newState) {
elevator.setState(newState);
}
};
然后实现具体状态。以开门状态为例:
class OpenState : public ElevatorState {
public:
void openDoor(Elevator& elevator) override {
// 门已经开了,忽略重复操作
std::cout << "门已经是开启状态" << std::endl;
}
void closeDoor(Elevator& elevator) override {
std::cout << "正在关门..." << std::endl;
changeState(elevator, new ClosedState());
}
void run(Elevator& elevator) override {
// 开门状态下不能运行,这是非法操作
std::cout << "错误:门未关闭,无法运行!" << std::endl;
}
void stop(Elevator& elevator) override {
// 已经停止状态,忽略
std::cout << "电梯已停止" << std::endl;
}
std::string getStateName() const override {
return "开门状态";
}
};
运行状态类似:
class RunningState : public ElevatorState {
public:
void openDoor(Elevator& elevator) override {
std::cout << "错误:运行中无法开门!" << std::endl;
}
void closeDoor(Elevator& elevator) override {
std::cout << "门已关闭" << std::endl;
}
void run(Elevator& elevator) override {
std::cout << "电梯正在运行中" << std::endl;
}
void stop(Elevator& elevator) override {
std::cout << "正在停止..." << std::endl;
changeState(elevator, new StoppedState());
}
std::string getStateName() const override {
return "运行状态";
}
};
最后是电梯上下文类:
class Elevator {
private:
std::unique_ptr<ElevatorState> currentState;
public:
Elevator() : currentState(std::make_unique<StoppedState>()) {
// 初始状态:停止且门关闭
}
void setState(ElevatorState* newState) {
currentState.reset(newState);
std::cout << "状态切换为: " << currentState->getStateName() << std::endl;
}
// 委托给当前状态
void openDoor() { currentState->openDoor(*this); }
void closeDoor() { currentState->closeDoor(*this); }
void run() { currentState->run(*this); }
void stop() { currentState->stop(*this); }
std::string getCurrentState() const {
return currentState->getStateName();
}
};
使用示例
int main() {
Elevator elevator;
elevator.openDoor(); // 输出:错误:停止状态门是关闭的,无法开门
// 等等,这里有个问题——初始状态是停止,但停止状态下开门应该是允许的
// 正确的流程应该是:
elevator.stop(); // 先确保停止
elevator.openDoor(); // 开门
elevator.closeDoor(); // 关门
elevator.run(); // 运行
elevator.stop(); // 停止
return 0;
}
注意:上面的示例暴露了一个设计问题——初始状态的选择。我建议把初始状态设为"停止且门关闭",但开门操作在停止状态下应该是合法的。所以 StoppedState 的 openDoor 方法应该允许开门,而不是报错。这个细节很容易被忽略,我在代码评审时经常看到这种边界情况没处理好。
状态模式的优缺点
| 优点 | 缺点 |
|---|---|
| 状态转换逻辑集中,易于维护 | 状态类数量会增多 |
| 新增状态无需修改现有代码 | 状态类之间可能有依赖 |
| 消除了庞大的条件分支 | 状态对象创建可能频繁 |
| 每个状态独立测试 | 上下文和状态耦合较紧 |
我的经验:状态模式特别适合状态数量在 3-8 个之间的场景。少于 3 个用 if-else 就够了,多于 8 个建议考虑状态机框架。我曾经在一个通信协议解析项目中用了状态模式,状态多达 12 个,结果状态类之间的转换关系变得极其复杂,最后不得不引入状态机生成工具。
避坑指南
我曾经犯过一个低级错误——在状态类的析构函数里做状态切换。结果导致内存泄漏,因为 new 出来的新状态还没来得及赋值就被析构了。记住:状态切换一定要在方法内部完成,不要在析构函数里搞事情。
另一个常见坑是状态对象的生命周期管理。我建议用 std::unique_ptr 或 std::shared_ptr,别裸用 new/delete。现代 C++ 里,智能指针是你的好朋友。
最后,别忘了考虑线程安全。如果电梯系统是多线程的,状态切换时需要加锁。我见过一个线上事故,就是因为两个线程同时触发了状态切换,导致电梯状态变成了"开门且运行"——想想都吓人。
状态模式说白了就是把行为封装到状态里,让每个状态自己决定该做什么。你想想看,这其实特别符合现实世界的逻辑——人在不同状态下行为就是不一样,电梯也一样。掌握了这个模式,以后再遇到复杂的状态逻辑,你就能写出既优雅又健壮的代码了。