一、背景
现实世界,若物体只存在有限状态,且状态改变时,物体行为随之改变,则需用「状态模式」描述。
下文举例,例如有某文档类(Document),其会属于 草稿(Draft)、审阅中(Moderation)、已发布(Published)三种状态之一。在不同状态时, 「发布(publish)方法」表现不同:
- 当在 Draft 状态时:会转换到 Moderation 状态。
- 当在 Modetation 状态时:若为管理员,则会转换到 Published 状态(即公开发布文档)。
- 当在 Published 状态时:无操作。
状态模式通常由很多 if 和 else 实现,通常表现为一组成员变量,例如下文。当功能扩展时,简直难以维护,故需要状态机:
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
// ……
二、结构
状态模式建议为对象的所有可能状态新建一个「类」, 然后将所有状态的对应行为抽取到这些类中。
原始对象称为「上下文」,其不会自行实现所有行为,而会保存一个指向「表示当前状态的对象 state」的引用,并把所有与状态相关的工作都「委派给该对象」。
例如下文,Document 是文档类,其有 state 属性:
- Document.render 即为 Document.state.render
- Document.publish 即为 Document.state.publish
- Document 还有 changeState() 方法,可切换其 state 属性
各状态均为类:
- 且均实现了 state 的 interface:
- Drate 类,实现了 state 的 interface
- 其 render() 实现为:若为管理员或作者则渲染,否则报错
- 其 publish() 实现为:若为管理员则切换至 Publishd state,否则切换至 Moderation state
- Moderation 类,实现了 state 的 interface
- Published 类,实现了 state 的 interface
- Drate 类,实现了 state 的 interface
- 且含 document 的属性,构造函数会传入该属性,如 new Published(document),用于获取上下文,和触发状态转移
其宏观特点和架构图如下:
- 各状态类,都需遵循「同样的接口」,上下文必须「通过此接口」与这些类交互
- 看上去和策略模式很像,但区别如下:
- 状态模式中,各状态知道其他状态的存在(这些类会「状态转移」到其他类)
- 策略模式中,各策略不知道其他策略的存在
2.1 播放器案例
本例,播放器,根据当前状态,实现不同的行为:
// 音频播放器(AudioPlayer)类即为上下文。它还会维护指向状态类实例的引用,
// 该状态类则用于表示音频播放器当前的状态。
class AudioPlayer is
field state: State
field UI, volume, playlist, currentSong
constructor AudioPlayer() is
this.state = new ReadyState(this)
// 上下文会将处理用户输入的工作委派给状态对象。由于每个状态都以不
// 同的方式处理输入,其结果自然将依赖于当前所处的状态。
UI = new UserInterface()
UI.lockButton.onClick(this.clickLock)
UI.playButton.onClick(this.clickPlay)
UI.nextButton.onClick(this.clickNext)
UI.prevButton.onClick(this.clickPrevious)
// 其他对象必须能切换音频播放器当前所处的状态。
method changeState(state: State) is
this.state = state
// UI 方法会将执行工作委派给当前状态。
method clickLock() is
state.clickLock()
method clickPlay() is
state.clickPlay()
method clickNext() is
state.clickNext()
method clickPrevious() is
state.clickPrevious()
// 状态可调用上下文的一些服务方法。
method startPlayback() is
// ……
method stopPlayback() is
// ……
method nextSong() is
// ……
method previousSong() is
// ……
method fastForward(time) is
// ……
method rewind(time) is
// ……
// 所有具体状态类都必须实现状态基类声明的方法,并提供反向引用指向与状态相
// 关的上下文对象。状态可使用反向引用将上下文转换为另一个状态。
abstract class State is
protected field player: AudioPlayer
// 上下文将自身传递给状态构造函数。这可帮助状态在需要时获取一些有用的
// 上下文数据。
constructor State(player) is
this.player = player
abstract method clickLock()
abstract method clickPlay()
abstract method clickNext()
abstract method clickPrevious()
// 具体状态会实现与上下文状态相关的多种行为。
class LockedState extends State is
// 当你解锁一个锁定的播放器时,它可能处于两种状态之一。
method clickLock() is
if (player.playing)
player.changeState(new PlayingState(player))
else
player.changeState(new ReadyState(player))
method clickPlay() is
// 已锁定,什么也不做。
method clickNext() is
// 已锁定,什么也不做。
method clickPrevious() is
// 已锁定,什么也不做。
// 它们还可在上下文中触发状态转换。
class ReadyState extends State is
method clickLock() is
player.changeState(new LockedState(player))
method clickPlay() is
player.startPlayback()
player.changeState(new PlayingState(player))
method clickNext() is
player.nextSong()
method clickPrevious() is
player.previousSong()
class PlayingState extends State is
method clickLock() is
player.changeState(new LockedState(player))
method clickPlay() is
player.stopPlayback()
player.changeState(new ReadyState(player))
method clickNext() is
if (event.doubleclick)
player.nextSong()
else
player.fastForward(5)
method clickPrevious() is
if (event.doubleclick)
player.previous()
else
player.rewind(5)
2.2 自动售货机
三、使用场景
如果对象需要根据自身当前状态进行不同行为, 同时状态的数量非常多且与状态相关的代码会频繁变更的话, 可使用状态模式。
模式建议你将所有特定于状态的代码抽取到一组独立的类中。 这样一来, 你可以在独立于其他状态的情况下添加新状态或修改已有状态, 从而减少维护成本。
如果某个类需要根据成员变量的当前值改变自身行为, 从而需要使用大量的条件语句时, 可使用该模式。
状态模式会将这些条件语句的分支抽取到相应状态类的方法中。 同时, 你还可以清除主要类中与特定状态相关的临时成员变量和帮手方法代码。
当相似状态和基于条件的状态机转换中存在许多重复代码时, 可使用状态模式。
状态模式让你能够生成状态类层次结构, 通过将公用代码抽取到抽象基类中来减少重复。
四、实现方式
确定哪些类是上下文。 它可能是包含依赖于状态的代码的已有类; 如果特定于状态的代码分散在多个类中, 那么它可能是一个新的类。
声明状态接口。 虽然你可能会需要完全复制上下文中声明的所有方法, 但最好是仅把关注点放在那些可能包含特定于状态的行为的方法上。
为每个实际状态创建一个继承于状态接口的类。 然后检查上下文中的方法并将与特定状态相关的所有代码抽取到新建的类中。
在将代码移动到状态类的过程中, 你可能会发现它依赖于上下文中的一些私有成员。 你可以采用以下几种变通方式:
- 将这些成员变量或方法设为公有。
- 将需要抽取的上下文行为更改为上下文中的公有方法, 然后在状态类中调用。 这种方式简陋却便捷, 你可以稍后再对其进行修补。
- 将状态类嵌套在上下文类中。 这种方式需要你所使用的编程语言支持嵌套类。
在上下文类中添加一个状态接口类型的引用成员变量, 以及一个用于修改该成员变量值的公有设置器。
再次检查上下文中的方法, 将空的条件语句替换为相应的状态对象方法。
为切换上下文状态, 你需要创建某个状态类实例并将其传递给上下文。 你可以在上下文、 各种状态或客户端中完成这项工作。 无论在何处完成这项工作, 该类都将依赖于其所实例化的具体类。
五、优缺点
优点:
- 单一职责原则。 将与特定状态相关的代码放在单独的类中。
- 开闭原则。 无需修改已有状态类和上下文就能引入新状态。
- 通过消除臃肿的状态机条件语句简化上下文代码。
缺点:
- 如果状态机只有很少的几个状态, 或者很少发生改变, 那么应用该模式可能会显得小题大作。
和其他模式的关系:
- 桥接模式、 状态模式和策略模式 (在某种程度上包括适配器模式) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。
- 状态可被视为策略的扩展。 两者都基于组合机制: 它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。 策略使得这些对象相互之间完全独立, 它们不知道其他对象的存在。 但状态模式没有限制具体状态之间的依赖, 且允许它们自行改变在不同情景下的状态。