状态模式¶
状态模式是一种行为设计模式, 让你能在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。

问题¶
状态模式与有限状态机的概念紧密相关。

其主要思想是程序在任意时刻仅可处于几种有限的状态中。 在任何一个特定状态中, 程序的行为都不相同, 且可瞬间从一个状态切换到另一个状态。 不过, 根据当前状态, 程序可能会切换到另外一种状态, 也可能会保持当前状态不变。 这些数量有限且预先定义的状态切换规则被称为转移。
你还可将该方法应用在对象上。 假如你有一个 文档Document类。 文档可能会处于 草稿Draft 、 审阅中Moderation和 已发布Published三种状态中的一种。 文档的 publish发布方法在不同状态下的行为略有不同:
- 处于
草稿状态时, 它会将文档转移到审阅中状态。 - 处于
审阅中状态时, 如果当前用户是管理员, 它会公开发布文档。 - 处于
已发布状态时, 它不会进行任何操作。

状态机通常由众多条件运算符 ( if或 switch ) 实现, 可根据对象的当前状态选择相应的行为。 “状态” 通常只是对象中的一组成员变量值。 即使你之前从未听说过有限状态机,你也很可能已经实现过状态模式。 下面的代码应该能帮助你回忆起来。
class Document is
field state: string
// ...
method publish() is
switch (state)
"draft":
state = "moderation"
break
"moderation":
if (currentUser.role == 'admin')
state = "published"
break
"published":
// 什么也不做。
break
// ...
当我们逐步在 文档类中添加更多状态和依赖于状态的行为后, 基于条件语句的状态机就会暴露其最大的弱点。 为了能根据当前状态选择完成相应行为的方法, 绝大部分方法中会包含复杂的条件语句。 修改其转换逻辑可能会涉及到修改所有方法中的状态条件语句, 导致代码的维护工作非常艰难。
这个问题会随着项目进行变得越发严重。 我们很难在设计阶段预测到所有可能的状态和转换。随着时间推移, 最初仅包含有限条件语句的简洁状态机可能会变成臃肿的一团乱麻。
解决方案¶
状态模式建议为对象的所有可能状态新建一个类, 然后将所有状态的对应行为抽取到这些类中。
原始对象被称为上下文 (context), 它并不会自行实现所有行为, 而是会保存一个指向表示当前状态的状态对象的引用, 且将所有与状态相关的工作委派给该对象。

如需将上下文转换为另外一种状态, 则需将当前活动的状态对象替换为另外一个代表新状态的对象。 采用这种方式是有前提的: 所有状态类都必须遵循同样的接口, 而且上下文必须仅通过接口与这些对象进行交互。
这个结构可能看上去与策略模式相似, 但有一个关键性的不同——在状态模式中, 特定状态知道其他所有状态的存在, 且能触发从一个状态到另一个状态的转换; 策略则几乎完全不知道其他策略的存在。
状态模式结构¶

- 上下文 (Context) 保存了对于一个具体状态对象的引用, 并会将所有与该状态相关的工作委派给它。 上下文通过状态接口与状态对象交互, 且会提供一个设置器用于传递新的状态对象。
- 状态 (State) 接口会声明特定于状态的方法。 这些方法应能被其他所有具体状态所理解, 因为你不希望某些状态所拥有的方法永远不会被调用。
- 具体状态 (Concrete States) 会自行实现特定于状态的方法。 为了避免多个状态中包含相似代码,你可以提供一个封装有部分通用行为的中间抽象类。
- 状态对象可存储对于上下文对象的反向引用。 状态可以通过该引用从上下文处获取所需信息, 并且能触发状态转移。
- 上下文和具体状态都可以设置上下文的下个状态, 并可通过替换连接到上下文的状态对象来完成实际的状态转换。
真实世界类比¶
智能手机的按键和开关会根据设备当前状态完成不同行为:
- 当手机处于解锁状态时, 按下按键将执行各种功能。
- 当手机处于锁定状态时, 按下任何按键都将解锁屏幕。
- 当手机电量不足时, 按下任何按键都将显示充电页面。
代码示例¶
class Connection;
class State {
public:
virtual void open(const Connection&) const = 0;
virtual void close(const Connection&) const = 0;
virtual ~State() = default;
};
class Connection {
public:
Connection(std::unique_ptr<State> _p) : p(std::move(_p)) {}
void changeState(std::unique_ptr<State> _p)
{
p = std::move(_p);
}
void open() const
{
p->open(*this);
}
void close() const
{
p->close(*this);
}
private:
std::unique_ptr<State> p;
};
class StateA : public State {
public:
void open(const Connection&) const override
{
std::cout << 1 << std::endl;
}
void close(const Connection&) const override
{
std::cout << 2 << std::endl;
}
};
class StateB : public State {
public:
void open(const Connection&) const override
{
std::cout << 3 << std::endl;
}
void close(const Connection&) const override
{
std::cout << 4 << std::endl;
}
};
int main()
{
Connection connection{ std::make_unique<StateA>() };
connection.open(); //@ 1
connection.close(); //@ 2
connection.changeState(std::make_unique<StateB>());
connection.open(); //@ 3
connection.close(); //@ 4
}
状态模式总结¶
实现方式¶
确定哪些类是上下文。 它可能是包含依赖于状态的代码的已有类; 如果特定于状态的代码分散在多个类中, 那么它可能是一个新的类。
声明状态接口。 虽然你可能会需要完全复制上下文中声明的所有方法, 但最好是仅把关注点放在那些可能包含特定于状态的行为的方法上。
为每个实际状态创建一个继承于状态接口的类。 然后检查上下文中的方法并将与特定状态相关的所有代码抽取到新建的类中。
在将代码移动到状态类的过程中, 你可能会发现它依赖于上下文中的一些私有成员。 你可以采用以下几种变通方式:
- 将这些成员变量或方法设为公有。
- 将需要抽取的上下文行为更改为上下文中的公有方法, 然后在状态类中调用。 这种方式简陋却便捷, 你可以稍后再对其进行修补。
- 将状态类嵌套在上下文类中。 这种方式需要你所使用的编程语言支持嵌套类。
在上下文类中添加一个状态接口类型的引用成员变量, 以及一个用于修改该成员变量值的公有设置器。
再次检查上下文中的方法, 将空的条件语句替换为相应的状态对象方法。
为切换上下文状态, 你需要创建某个状态类实例并将其传递给上下文。 你可以在上下文、各种状态或客户端中完成这项工作。 无论在何处完成这项工作, 该类都将依赖于其所实例化的具体类。
优点¶
- 单一职责原则。 将与特定状态相关的代码放在单独的类中。
- 开闭原则。 无需修改已有状态类和上下文就能引入新状态。
- 通过消除臃肿的状态机条件语句简化上下文代码。
缺点¶
- 如果状态机只有很少的几个状态, 或者很少发生改变, 那么应用该模式可能会显得小题大作。
适用场景¶
- 对象的行为依赖于它的状态(如某些属性值),状态的改变将导致行为的变化。
- 在代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,并且导致客户类与类库之间的耦合增强。