状态模式(State)
意图
状态模式是一种行为型模式,让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。
它允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
问题
状态模式与有限状态机的概念紧密相关。
其主要思想是程序在任意时刻仅可处于几种有限的状态中。在任何一个特定状态中,程序的行为都不相同,且可瞬间从一个状态切换到另一个状态。不过,根据当前状态,程序可能会切换到另外一种状态,也可能会保持当前状态不变。这些数量有限且预先定义的状态切换规则被称为转移。
你还可将该方法应用在对象上。假如你有一个文档Document
类。文档可能会处于草稿Draft
、审阅中Moderation
和已发布Published
三种状态中的一种。文档的publish发布
方法在不同状态下的行为略有不同:
- 处于
草稿
状态时,它会将文档转移到审阅中状态。 - 处于
审阅中
状态时,如果当前用户是管理员,它会公开发布文档。 - 处于
已发布
状态时,它不会进行任何操作。
状态机通常由众多条件运算符(if
或switch
)实现,可根据对象的当前状态选择相应的行为。 “状态” 通常只是对象中的一组成员变量值。即使你之前从未听说过有限状态机,你也很可能已经实现过状态模式。下面的代码应该能帮助你回忆起来。
1 |
|
当我们逐步在文档
类中添加更多状态和依赖于状态的行为后,基于条件语句的状态机就会暴露其最大的弱点。为了能根据当前状态选择完成相应行为的方法,绝大部分方法中会包含复杂的条件语句。修改其转换逻辑可能会涉及到修改所有方法中的状态条件语句,导致代码的维护工作非常艰难。
这个问题会随着项目进行变得越发严重。我们很难在设计阶段预测到所有可能的状态和转换。随着时间推移,最初仅包含有限条件语句的简洁状态机可能会变成臃肿的一团乱麻。
解决方案
将本来是针对对象的操作,委托给了这个对象的状态操作。运用状态模式可以将代码中的if,else代码块抽离出来,每个if,else下的代码块都封装为状态的某一个方法,所有状态都必须实现状态接口,即实现抽离出来的所有方法,由各自去执行在该状态下应该实现的逻辑,包括不合理的逻辑,可以抛出异常,这样可以规避大量的if,else嵌套。
状态模式建议为对象的所有可能状态新建一个类,然后将所有状态的对应行为抽取到这些类中。
原始对象被称为上下文(context),它并不会自行实现所有行为,而是会保存一个指向表示当前状态的状态对象的引用,且将所有与状态相关的工作委派给该对象。
如需将上下文转换为另外一种状态,则需将当前活动的状态对象替换为另外一个代表新状态的对象。采用这种方式是有前提的:所有状态类都必须遵循同样的接口,而且上下文必须仅通过接口与这些对象进行交互。
这个结构可能看上去与策略模式相似,但有一个关键性的不同————在状态模式中,特定状态知道其他所有状态的存在,且能触发从一个状态到另一个状态的转换;策略则几乎完全不知道其他策略的存在。
结构
- 上下文(Context)保存了对于一个具体状态对象的引用,并会将所有与该状态相关的工作委派给它。上下文通过状态接口与状态对象交互,且会提供一个设置器用于传递新的状态对象。
- 状态(State)接口会声明特定于状态的方法。这些方法应能被其他所有具体状态所理解,因为你不希望某些状态所拥有的方法永远不会被调用。
- 具体状态(Concrete States)会自行实现特定于状态的方法。为了避免多个状态中包含相似代码,你可以提供一个封装有部分通用行为的中间抽象类。
状态对象可存储对于上下文对象的反向引用。状态可以通过该引用从上下文处获取所需信息,并且能触发状态转移。 - 上下文和具体状态都可以设置上下文的下个状态,并可通过替换连接到上下文的状态对象来完成实际的状态转换。
实现方式
- 确定哪些类是上下文。 它可能是包含依赖于状态的代码的已有类; 如果特定于状态的代码分散在多个类中, 那么它可能是一个新的类。
- 声明状态接口。 虽然你可能会需要完全复制上下文中声明的所有方法, 但最好是仅把关注点放在那些可能包含特定于状态的行为的方法上。
- 为每个实际状态创建一个继承于状态接口的类。 然后检查上下文中的方法并将与特定状态相关的所有代码抽取到新建的类中。
在将代码移动到状态类的过程中, 你可能会发现它依赖于上下文中的一些私有成员。 你可以采用以下几种变通方式:- 将这些成员变量或方法设为公有。
- 将需要抽取的上下文行为更改为上下文中的公有方法, 然后在状态类中调用。 这种方式简陋却便捷, 你可以稍后再对其进行修补。
- 将状态类嵌套在上下文类中。 这种方式需要你所使用的编程语言支持嵌套类。
- 在上下文类中添加一个状态接口类型的引用成员变量, 以及一个用于修改该成员变量值的公有设置器。
- 再次检查上下文中的方法, 将空的条件语句替换为相应的状态对象方法。
- 为切换上下文状态, 你需要创建某个状态类实例并将其传递给上下文。 你可以在上下文、 各种状态或客户端中完成这项工作。 无论在何处完成这项工作, 该类都将依赖于其所实例化的具体类。
代码演示
1 |
|
执行结果:
1 |
|
参考原文:状态设计模式