状态这东西。
比“状态模式”这个词老得多。
我第一次被“状态”坑。
不是在 GoF。
是在做网络。
那会儿我还年轻。
觉得连接嘛。
就两种。
连上。
没连上。
后来现实很快教会我。
连接其实像谈恋爱。
没开始。
正在聊。
已经在一起。
冷战。
分手。
然后又复合。
你用一句 bool connected 去描述它。
就是用一把小螺丝刀去修发动机。
拧得动。
但手会断。
状态机的历史更早。
早到不是从软件来的。
是从电话交换机。
从电报码。
从那些“按下去会响”的机械世界来的。
后来理论界给它们起了名字。
Mealy 机。
Moore 机。
再后来。
协议栈、编译器、GUI 框架把它们捡了回来。
你会发现。
很多系统其实一直在做一件事。
接收事件。
然后根据“当前所处阶段”做不同的反应。
这就是 State。
不是玄学。
是把一堆散落的 if 和 switch 收拾起来。
让代码像个成年人。
知道自己现在处在哪。
也知道下一步该去哪。
你写的不是 if-else。 你写的是“阶段的迁移”。
先从最常见的灾难讲起:一个会长胖的 switch
假设你要写一个非常朴素的客户端。
能连。
能断。
能发消息。
你大概率会先这么写。
enum class ConnState {
Disconnected,
Connecting,
Connected,
};
状态挺清楚。
然后上一个“诚实”的 switch。
class Client {
public:
void open();
void close();
void send(std::string_view msg);
private:
ConnState state_ = ConnState::Disconnected;
};
实现也不难。
void Client::send(std::string_view msg) {
switch (state_) {
case ConnState::Connected:
// socket_.write(msg);
break;
default:
throw std::runtime_error("not connected");
}
}
到这里。
你会觉得一切都很好。
甚至觉得“模式都是多余的”。
然后需求来了。
“连接要有超时。”
“超时要重试。”
“重试要退避。”
“退避要区分错误码。”
“还要支持手动取消。”
你很快就会加新状态。
Backoff。
Closing。
Reconnecting。
然后每个函数里开始复制粘贴。
每次加一个事件。
你要去改所有 switch。
漏一个。
线上就会给你写情书。
而且是那种凌晨三点的。
真正恶心的点。
不是 switch 大。
是“跳转逻辑”散在各处。
你找一个迁移。
要在三个函数里翻来翻去。
你想加日志。
不知道该在哪打。
你想写测试。
发现只能整合测试。
单测根本没抓手。
State 模式做的事:把“阶段”变成对象
GoF 那套 State。
长得很像 Strategy。
都是一个 Context。
里面挂一个接口。
然后把调用转发出去。
差别在一句话。
Strategy 是你在“换算法”。
State 是对象在“换人格”。
同一个按钮。
在不同状态下。
点下去是完全不同的事。
最典型的例子。
就是连接。
断开时你点“发送”。
它应该报错。
连接中你点“发送”。
它可能要排队。
连接上你点“发送”。
它才真的发出去。
我们先给状态一个接口。
class Client;
struct State {
virtual ~State() = default;
virtual void open(Client&) {}
virtual void close(Client&) {}
virtual void send(Client&, std::string_view) {
throw std::runtime_error("operation not allowed in current state");
}
virtual const char* name() const = 0;
};
别急着嫌 virtual。
在“行为随阶段变化”的地方。
它确实好用。
然后 Client 变得很瘦。
它只做转发。
再加一个“换状态”的入口。
class Client {
public:
Client();
void open() { state_->open(*this); }
void close() { state_->close(*this); }
void send(std::string_view msg) { state_->send(*this, msg); }
void setState(std::unique_ptr<State> s) { state_ = std::move(s); }
const char* stateName() const { return state_->name(); }
private:
std::unique_ptr<State> state_;
};
你会发现。
Context 现在很“稳定”。
它不再被每个状态的分支撕扯。
它像个中枢。
只负责把电话转过去。
真正的行为。
在状态对象里。
我们写一个断开状态。
struct Disconnected final : State {
const char* name() const override { return "Disconnected"; }
void open(Client& c) override;
void send(Client&, std::string_view) override {
throw std::runtime_error("send while disconnected");
}
};
再写一个已连接状态。
struct Connected final : State {
const char* name() const override { return "Connected"; }
void close(Client& c) override;
void send(Client&, std::string_view msg) override {
// socket_.write(msg);
(void)msg;
}
};
关键在“迁移”。
状态自己知道下一步去哪。
void Disconnected::open(Client& c) {
// socket_.connect();
c.setState(std::make_unique<Connected>());
}
void Connected::close(Client& c) {
// socket_.close();
c.setState(std::make_unique<Disconnected>());
}
你看。
跳转逻辑回到了该在的地方。
从此以后。
你加一个新状态。
只需要写它自己的行为。
以及它会往哪跳。
你不用在所有函数里挨个补 case。
这就是 State 的“值”。
它不是减少代码。
它是减少你在脑子里做的 join。
这东西为什么让高手也愿意用:它让边界变清楚
当你用 switch。
你其实把所有状态揉成了一坨。
每个函数都知道所有状态。
每个状态也被迫知道所有函数。
耦合像蜘蛛网。
你扯一根。
全网都震。
State 把耦合切开。
每个状态只负责自己的那点事。
你甚至可以单独测试一个状态。
用假的 Client。
或者把 Client 的依赖做成可注入。
这时候。
你开始能写出“像样的测试”。
而不是祈祷线上别出事。
State 和 Strategy 到底差在哪
这俩长得像。
所以经常被混用。
我自己也踩过。
差别不在 UML。
差别在“变化的驱动力”。
Strategy 的变化。
通常来自外部选择。
配置。
业务规则。
AB 实验。
你把“怎么做”换掉。
但是 Context 的阶段没变。
State 的变化。
来自对象内部生命周期。
你连上了。
你断开了。
你超时了。
你重试耗尽了。
对象在经历事情。
它的身份在变。
所以 State 里。
状态对象经常会触发迁移。
这不是副作用。
这是主题。
Strategy 更像换工具。 State 更像换身份。
现代 C++ 的另一条路:std::variant 写“显式状态机”
有些同学讨厌堆分配。
也不想 virtual。
这很合理。
如果你的状态集合是固定的。
而且不会在运行时扩展。
std::variant 往往更干净。
先把状态变成类型。
struct Disconnected {};
struct Connected {};
using ConnState = std::variant<Disconnected, Connected>;
Client 持有一个 variant。
class Client {
public:
void open();
void close();
private:
ConnState state_ = Disconnected{};
};
迁移就是赋值。
void Client::open() {
std::visit([&](auto& s) {
using S = std::decay_t<decltype(s)>;
if constexpr (std::is_same_v<S, Disconnected>) {
state_ = Connected{};
}
}, state_);
}
这写法。
更像“把非法状态变成不可表示”。
你少了很多运行时分支。
也少了对象生命周期的绕来绕去。
代价也真实。
代码会更模板味。
状态一多。
visit 也会长胖。
所以我通常这样选。
状态可扩展。
需要插件式新增。
就用 GoF 口味的 State。
状态固定。
追求零堆分配。
就考虑 variant。
最后说一句老实话:别把一切都写成状态机
状态模式不是银弹。
它能救你。
也能害你。
当你把每个小分支都抽成状态。
你会得到一堆类。
看起来很“面向对象”。
但读起来像翻电话簿。
我更喜欢把它用在两类地方。
生命周期很明确。
而且事件真的会改变行为。
比如协议。
比如连接。
比如订单。
比如交互式 UI。
你会发现。
这些地方不写 State。
最后都会写成 State。
只是你写的是“隐藏版”。
藏在一堆 switch 里。
藏在 if-else 里。
藏在你的脑子里。
把它拿出来。
放到代码里。
让下一位维护的人少熬夜。
这事儿挺值的。