1 意图
允许一个其对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
2 别名
状态对象(Objects For States)
3 动机
考虑一个表示网络连接的类TCPConnection。一个TCPConnection对象的状态处于若干不同状态之一:连接已建立(Established)、正在监听(Listening)、连接已关闭(Closed)。当一个TCPConnection对象收到其他对象的请求时,它根据自身的当前状态作出不同的反应。例如,一个Open请求的结果依赖于该连接是处于连接已关闭状态还是连接已建立状态。State模式描述了TCPConnection如何在每一种状态下表现出不同的行为。
这一模式的关键思想是引入了一个称为TCPState的抽象类来表示网络的连接状态。TCPState类为各表示不同的操作状态的子类声明了一个公共接口。TCPState的子类实现与特定状态相关的行为。例如,TCPEstablished和TCPClosed类分别实现了特定于TCPConnection的连接已建立状态和连接已关闭状态的行为。
TCPConnection类维护一个表示TCP连接当前状态的状态对象(一个TCPState子类的实例)。TCPConnection类将所有与状态相关的请求委托给这个状态对象。TCPConnection使用它的TCPState子类实例来执行特定于连接状态的操作。
一旦连接状态改变,TCPConnection对象就会改变它所使用的状态对象。例如当连接从已建立状态转为已关闭状态时,TCPConnection会用一个TCPClosed的实例来代替原来的TCPEstablished的实例。
4 适用性
在下面的两种情况下均可使用State模式:
- 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为;
- 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常 , 有多个操作包含这一相同的条件结构。state模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。
5 结构
6 参与者
- Context(环境,如TCPConnection)
——定义客户感兴趣的接口
——维护一个ConcreteState子类的实例,这个实例定义当前的状态 - State(状态,如TCPState)
——定义一个接口以封装与Context的一个特定状态相关的行为 - ConcreteState subclasses(具体状态子类,如TCPEstablished,TCPListen,TCPClosed)
——每一子类实现一个与Context的一个状态相关的行为
7 协作
- Context将与状态相关的请求委托给当前的ConcreteState对象处理;
- Context可将自身作为一个参数传递给处理该请求的状态对象。这使得状态对象在必要时可访问Context;
- Context是客户使用的主要接口。客户可用状态对象来配置一个Context,一旦一个Context配置完毕, 它的客户不再需要直接与状态对象打交道。
- Context或ConcreteState子类都可决定哪个状态是另外哪一个的后继者,以及是在何种条件下进行条件转换。
8 效果
State模式有下面一些效果:
- 1 它将与特定状态相关的行为局部化,并且将不同状态的行为分割开来
- 2 它使得状态转换显式化:当一个对象仅以内部数据值来定义当前状态时 , 其状态仅表现为对一些变量的赋值,这不够明确。为不同的状态引入独立的对象使得转换变得更加明确。而且, State对象可保证Context不会发生内部状态不一致的情况,因为从Context的角度看,状态转换是原子的 — 只需重新绑定一个变量,(即Context的State对象变量),而无需为多个变量赋值。
- 3 State对象可被共享:如果State对象没有实例变量 — 即它们表示的状态完全以它们的类型来编码 — 那么各Context对象可以共享一个State对象。当状态以这种方式被共享时 , 它们必然是没有内部状态, 只有行为的轻量级对象。
9 实现
实现State模式有多方面的考虑:
- 1 谁定义状态转换:State模式不指定哪一个参与者定义状态转换准则。如果该准则是固定的, 那么它们可在Context中完全实现。然而若让State子类自身指定它们的后继状态以及何时进行转换 , 通常更灵活更合适。这需要Context增加一个接口 , 让State对象显式地设定Context的当前状态。
用这种方法分散转换逻辑可以很容易地定义新的State子类来修改和扩展该逻辑。 这样做的一个缺点是,一个State子类至少拥有一个其他子类的信息 , 这就再各子类之间产生了实现依赖。 - 2 基于表的另一种方法:在C++ Programming Style中,Cargil描述了另一种将结构加载在状态驱动的代码上的方法 : 他使用表将输入映射到状态转换。对每一个状态 , 一张表将每一个可能的输入映射到一个后继状态。实际上 , 这种方法将条件代码 (和State模式下的虚函数)映射为一个查找表。
表的主要好处是它们的规则性 : 你可以通过更改数据而不是更改程序代码来改变状态转换的准则。然而它也有一些缺点:- 对表的查找通常不如(虚)函数调用效率高;
- 用统一的、表格的形式表示转换逻辑使得转换准则变得不够明确而难以理解;
- 通常难以加入伴随状态转换的一些动作。表驱动的方法描述了状态和它们之间的转换,但必须扩充这个机制以便在每一个转换上能够进行任意的计算。
表驱动的状态机和State模式的主要区别可以被总结如下: State模式对与状态相关的行为进行建模, 而表驱动的方法着重于定义状态转换。
- 3 创建和销毁State对象:一个常见的值得考虑的实现上的权衡是 , 究竟是( 1 )仅当需要State对象时才创建它们并随后销毁它们,还是 ( 2 )提前创建它们并且始终不销毁它们。
当将要进入的状态在运行时是不可知的 , 并且上下文不经常改变状态时 , 第一种选择较为可取。这种方法避免创建不会被用到的对象 , 如果State对象存储大量的信息时这一点很重要。当状态改变很频繁时, 第二种方法较好。在这种情况下最好避免销毁状态 , 因为可能很快再次需要用到它们。此时可以预先一次付清创建各个状态对象的开销 , 并且在运行过程中根本不存在销毁状态对象的开销。但是这种方法可能不太方便 , 因为Context必须保存对所有可能会进入的那些状态的引用。 - 4 使用动态继承:改变一个响应特定请求的行为可以用在运行时刻改变这个对象的类的办法实现, 但这在大多数面向对象程序设计语言中都是不可能的。 Self 和其他一些基于委托的语言却是例外,它们提供这种机制 , 从而直接支持State模式。Self 中的对象可将操作委托给其他对象以达到某种形式的动态继承。在运行时刻改变委托的目标有效地改变了继承的结构。这一机制允许对象改变它们的行为,也就是改变它们的类。