01 意图
状态是一种行为设计模式,它允许对象在其内部状态发生变化时改变其行为。看起来好像对象改变了它的类。
02 问题
状态模式与有限状态机的概念密切相关。
主要思想是,在任何给定时刻,程序可以处于有限数量的状态。在任何独特的状态中,程序的行为都不同,并且程序可以立即从一种状态切换到另一种状态。然而,根据当前状态,程序可能会或可能不会切换到某些其他状态。这些切换规则,称为转移,也是有限的和预先确定的。
您也可以将此方法应用于对象。想象一下,我们有一Document
堂课。文档可以处于以下三种状态之一Draft
:Moderation
和Published
。文档的publish
方法在每个州的工作方式都略有不同:
在
Draft
中,它将文档移至审核。在
Moderation
中,它使文档公开,但前提是当前用户是管理员。在
Published
中,它根本不做任何事情。
状态机通常使用许多条件运算符(if
或switch
)来实现,它们根据对象的当前状态选择适当的行为。通常,这个“状态”只是对象字段的一组值。即使您以前从未听说过有限状态机,您也可能至少实现过一次状态。以下代码结构是否敲响了警钟?
// The AudioPlayer class acts as a context. It also maintains a
// reference to an instance of one of the state classes that
// represents the current state of the audio player.
class AudioPlayer is
field state: State
field UI, volume, playlist, currentSong
constructor AudioPlayer() is
this.state = new ReadyState(this)
// Context delegates handling user input to a state
// object. Naturally, the outcome depends on what state
// is currently active, since each state can handle the
// input differently.
UI = new UserInterface()
UI.lockButton.onClick(this.clickLock)
UI.playButton.onClick(this.clickPlay)
UI.nextButton.onClick(this.clickNext)
UI.prevButton.onClick(this.clickPrevious)
// Other objects must be able to switch the audio player's
// active state.
method changeState(state: State) is
this.state = state
// UI methods delegate execution to the active state.
method clickLock() is
state.clickLock()
method clickPlay() is
state.clickPlay()
method clickNext() is
state.clickNext()
method clickPrevious() is
state.clickPrevious()
// A state may call some service methods on the context.
method startPlayback() is
// ...
method stopPlayback() is
// ...
method nextSong() is
// ...
method previousSong() is
// ...
method fastForward(time) is
// ...
method rewind(time) is
// ...
// The base state class declares methods that all concrete
// states should implement and also provides a backreference to
// the context object associated with the state. States can use
// the backreference to transition the context to another state.
abstract class State is
protected field player: AudioPlayer
// Context passes itself through the state constructor. This
// may help a state fetch some useful context data if it's
// needed.
constructor State(player) is
this.player = player
abstract method clickLock()
abstract method clickPlay()
abstract method clickNext()
abstract method clickPrevious()
// Concrete states implement various behaviors associated with a
// state of the context.
class LockedState extends State is
// When you unlock a locked player, it may assume one of two
// states.
method clickLock() is
if (player.playing)
player.changeState(new PlayingState(player))
else
player.changeState(new ReadyState(player))
method clickPlay() is
// Locked, so do nothing.
method clickNext() is
// Locked, so do nothing.
method clickPrevious() is
// Locked, so do nothing.
// They can also trigger state transitions in the context.
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)
一旦我们开始向类中添加越来越多的状态和依赖于状态的行为,基于条件的状态机的最大弱点就会显现出来Document
。大多数方法将包含可怕的条件,这些条件会根据当前状态选择方法的正确行为。这样的代码很难维护,因为对转换逻辑的任何更改都可能需要在每个方法中更改状态条件。
随着项目的发展,问题往往会变得更大。在设计阶段预测所有可能的状态和转换是相当困难的。因此,用一组有限的条件构建的精益状态机会随着时间的推移变得臃肿不堪。
03 解决方案
状态模式建议您为对象的所有可能状态创建新类,并将所有特定于状态的行为提取到这些类中。
原始对象(称为context )不是自己实现所有行为,而是存储对表示其当前状态的状态对象之一的引用,并将所有与状态相关的工作委托给该对象。
要将上下文转换为另一种状态,请将活动状态对象替换为表示该新状态的另一个对象。只有当所有状态类都遵循相同的接口并且上下文本身通过该接口与这些对象一起工作时,这才有可能。
这种结构可能看起来类似于策略模式,但有一个关键区别。在状态模式中,特定状态可能会相互了解并启动从一种状态到另一种状态的转换,而策略几乎从不知道彼此。
04 举个栗子
根据设备的当前状态,智能手机中的按钮和开关的行为会有所不同:
当手机解锁时,按下按钮会导致执行各种功能。
当手机被锁定时,按任何按钮都会进入解锁屏幕。
手机电量低时,按任意键显示充电画面。
05 结构实现
在此示例中,状态模式让媒体播放器的相同控件表现不同,具体取决于当前播放状态。
播放器的主要对象始终链接到为播放器执行大部分工作的状态对象。一些动作用另一个动作替换了玩家的当前状态对象,这改变了玩家对用户交互的反应方式。
// The AudioPlayer class acts as a context. It also maintains a
06 适用场景
- 当您的对象根据其当前状态表现不同、状态数量巨大且特定于状态的代码经常更改时,请使用状态模式。
该模式建议您将所有特定于状态的代码提取到一组不同的类中。因此,您可以相互独立地添加新状态或更改现有状态,从而降低维护成本。
- 当您的类被大量条件污染时使用该模式,这些条件会根据类字段的当前值改变类的行为方式。
状态模式允许您将这些条件的分支提取到相应状态类的方法中。这样做时,您还可以从主类中清除状态特定代码中涉及的临时字段和辅助方法。
- 当您在基于条件的状态机的相似状态和转换中有大量重复代码时,请使用 State。
状态模式允许您组合状态类的层次结构并通过将公共代码提取到抽象基类中来减少重复。
07 如何实施
决定哪个类将作为上下文。它可能是已经具有状态相关代码的现有类;或者一个新的类,如果特定于状态的代码分布在多个类中。
声明状态接口。尽管它可以反映上下文中声明的所有方法,但仅针对那些可能包含特定于状态的行为的方法。
-
对于每个实际状态,创建一个派生自状态接口的类。然后检查上下文的方法并将与该状态相关的所有代码提取到新创建的类中。
在将代码移动到状态类时,您可能会发现它依赖于上下文的私有成员。有几种解决方法:
公开这些字段或方法。
将您提取的行为转换为上下文中的公共方法,并从状态类中调用它。这种方式很丑但很快,你以后总是可以修复它。
将状态类嵌套到上下文类中,但前提是您的编程语言支持嵌套类。
在上下文类中,添加状态接口类型的引用字段和允许覆盖该字段值的公共设置器。
再次检查上下文的方法,并将空状态条件替换为对状态对象相应方法的调用。
要切换上下文的状态,请创建状态类之一的实例并将其传递给上下文。您可以在上下文本身、各种状态或客户端中执行此操作。无论在哪里完成,该类都依赖于它实例化的具体状态类。
08 优缺点
关注公众号可获取干货资料:《设计模式详解》