设计模式之状态(State)模式
意图
状态(State)模式是一种行为设计模式,允许对象在内部状态改变时修改器对象行为。使其看上去就像改变了自身所属的类一样。
问题描述
状态模式与有限状态机的概念紧密相关。
主要思想是,在任何给定的时刻,程序都可以处于有限的状态。在任何唯一的状态下,程序的行为都不同,程序可以瞬间从一种状态切换到另一种状态。然而,根据当前状态,程序可以切换到某些其他状态,也可以不切换到某些状态。这些被称为转换的切换规则也是有限的和预先确定的。
你也可以将此方法应用于对象。假设我们有一个 Document
类。文档可以处于以下三种状态之一:草稿(Draft
)、审阅(Moderation
)和已发布(Published
)。文档的发布(publish
)方法在每个状态下的工作方式略有不同:
- 在草稿(
Draft
)状态下,它将文档推向审核。 - 在审核(
Moderation
)状态下,如果当前用户是管理员,它会将文档公开。 - 在发布(
Published
)状态下,它不会有任何操作。
状态机通常由许多条件语句(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":
// Do nothing.
break
// ...
一旦我们开始向 Document
类添加越来越多的状态和依赖于状态的行为,基于条件的状态机的最大弱点就会暴露出来。大多数方法都会包含可怕的条件句,这些条件句根据当前状态选择方法的正确行为。像这样的代码很难维护,因为对转换逻辑的任何更改都可能需要更改每个方法中的状态条件。
随着项目的发展,问题往往会变得更大。在设计阶段很难预测所有可能的状态和转变。因此,随着时间的推移,用有限的条件集构建的状态机可能会变得一团糟。
方案
状态(State)模式建议为对象的所有可能状态创建新的类,并将所有特定于状态的行为提取到这些类中。
称为上下文(context)的原始对象不自己实现所有行为,而是保存一个表示其当前状态的状态对象的引用,并将所有与状态相关的工作委派给该对象。
若要将上下文转换为另一种状态,请将活动状态对象替换为表示该新状态的另一个对象。只有当所有状态类都遵循相同的接口,并且上下文本身通过该接口与这些对象一起工作时,这才有可能实现。
这种结构看起来可能与策略(Strategy)模式相似,但有一个关键区别。在状态模式中,特定状态可能相互了解,并启动从一个状态到另一个状态的转换,而策略几乎从不了解彼此。
真实世界类比
智能手机中的按钮和开关根据设备的当前状态表现不同:
- 当手机解锁时,按下按钮可执行各种功能。
- 当手机被锁定时,按下任何按钮都会进入解锁屏幕。
- 当手机电量不足时,按下任何按钮都会显示充电屏幕。
结构
上下文(Context)保存一个具体状态对象的引用并委派所有状态相关的任务给它。上下文通过状态接口与状态对象进行通信。上下文暴露一个 setter 用来传递新的状态对象。
状态(State)接口声明了特定于状态的方法。这些方法应能被其他所有具体状态所理解, 因为你不希望某些状态所拥有的方法永远不会被调用。
具体状态(Concrete State)提供了它们对特定状态方法的实现。为避免多个状态相似代码的复制,你可以提供封装一些通用行为的中介抽象类。
状态对象可能存储一个上下文对象的反向引用。通过该引用,状态可以从上下文中获取所需信息,以及发起状态转移。
上下文和具体状态都可以设置上下文的下一个状态,并通过替换链接到上下文的状态对象来执行实际的状态转换。
伪代码
本例中,状态(State)模式允许允许媒体播放器的相同控件根据当前播放状态表现不同。
玩家(player)的主对象总是链接到为玩家执行大部分工作的状态对象。一些操作将玩家的当前状态对象替换为另一个,从而改变玩家对用户交互的反应方式。
// 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)
适用
如果对象的行为因其当前状态而异,状态数量巨大,并且特定于状态的代码频繁更改,请使用状态模式。
该模式建议你将所有特定于状态的代码提取到一组不同的类中。因此,你可以相互独立地添加新状态或更改现有状态,从而降低维护成本。T
当类被大量条件污染时,请使用该模式,这些条件会根据类字段的当前值来改变类的行为方式。
状态(State)模式允许你将这些条件语句的分支提取到相应状态类的方法中。在这样做的同时,你还可以从主类中清除状态特定代码中涉及的临时字段和辅助方法。
当相似状态和基于条件的状态机转换中存在许多重复代码时, 可使用状态模式
状态模式允许你生成状态类的层次结构,并通过将公共代码提取到抽象基类中来减少重复。
如何实现
确定什么类作为上下文。它可以是已经有状态依赖代码的现有类,或者如果状态特定代码分散在多个类中时,可以是一个新的类。
声明状态接口。虽然它复刻了所有上下文中声明的方法,但只关注在那些可能包含特定状态的行为的方法上。
对于每个实际状态,创建一个从状态接口派生的类。然后检查上下文的方法,并将与该状态相关的所有代码提取到新创建的类中。
当将所有代码移到状态类时,你可能会发现它依赖于上下文的私有成员。这种情况有几种解决方式:
- 将这些字段或方法改为公开。
- 将要提取的行为转换为上下文中的公共方法,并在状态类中调用它。这种方法很难看,但速度很快,而且你以后总能修复。
- 将这些状态类嵌入到上下文类中,不过这种情况只有在所使用的编程语言支持嵌入类时有效。
- 在上下文类中,添加状态接口类型的引用字段以及允许重写该字段值的公开 setter。
再次检查上下文的方法,并将空状态条件替换为对状态对象的相应方法的调用。
要切换上下文的状态,请创建其中一个状态类的实例并将其传递给上下文。你可以在上下文本身、各种状态或客户端中执行此操作。无论在哪里执行此操作,类都将依赖于它实例化的具体状态类。
优缺点
- ✔️单一职责原则。将特定状态的代码组织到单独的类中。
- ✔️开闭原则。可以在不修改现有状态类或上下文的情况下引入新的状态。
- ❌通过消除臃肿的状态机条件语句简化上下文代码。
- ❌如果状态机只有很少的几个状态, 或者很少发生改变, 那么应用该模式可能会显得小题大作
与其他模式的关系
- 桥接模式、状态模式、策略模式(以及在某种程度上的适配器模式)具有非常相似的结构。事实上,所有这些模式都是基于组合的,即将工作委派给其他对象。然而,它们都能解决不同的问题。模式不仅仅是以特定方式构建代码的配方。它还可以与其他开发人员交流模式所解决的问题。
- 状态(State)模式被当作策略(Strategy)模式的扩展。这两个模式都是基于组合:它们通过委派任务给助手对象修改上下文的行为。策略模式使得这些对象完全独立,且相互之间不知道对方的存在。不过,状态模式的不同实体状态并没有限制依赖,使之可以任意修改上下文的状态。c
代码示例
index.php: 概念示例
<?php
namespace RefactoringGuru\State\Conceptual;
/**
* The Context defines the interface of interest to clients. It also maintains a
* reference to an instance of a State subclass, which represents the current
* state of the Context.
*/
class Context
{
/**
* @var State A reference to the current state of the Context.
*/
private $state;
public function __construct(State $state)
{
$this->transitionTo($state);
}
/**
* The Context allows changing the State object at runtime.
*/
public function transitionTo(State $state): void
{
echo "Context: Transition to " . get_class($state) . ".\n";
$this->state = $state;
$this->state->setContext($this);
}
/**
* The Context delegates part of its behavior to the current State object.
*/
public function request1(): void
{
$this->state->handle1();
}
public function request2(): void
{
$this->state->handle2();
}
}
/**
* The base State class declares methods that all Concrete State should
* implement and also provides a backreference to the Context object, associated
* with the State. This backreference can be used by States to transition the
* Context to another State.
*/
abstract class State
{
/**
* @var Context
*/
protected $context;
public function setContext(Context $context)
{
$this->context = $context;
}
abstract public function handle1(): void;
abstract public function handle2(): void;
}
/**
* Concrete States implement various behaviors, associated with a state of the
* Context.
*/
class ConcreteStateA extends State
{
public function handle1(): void
{
echo "ConcreteStateA handles request1.\n";
echo "ConcreteStateA wants to change the state of the context.\n";
$this->context->transitionTo(new ConcreteStateB());
}
public function handle2(): void
{
echo "ConcreteStateA handles request2.\n";
}
}
class ConcreteStateB extends State
{
public function handle1(): void
{
echo "ConcreteStateB handles request1.\n";
}
public function handle2(): void
{
echo "ConcreteStateB handles request2.\n";
echo "ConcreteStateB wants to change the state of the context.\n";
$this->context->transitionTo(new ConcreteStateA());
}
}
/**
* The client code.
*/
$context = new Context(new ConcreteStateA());
$context->request1();
$context->request2();
Output.txt: 执行结果
Context: Transition to RefactoringGuru\State\Conceptual\ConcreteStateA.
ConcreteStateA handles request1.
ConcreteStateA wants to change the state of the context.
Context: Transition to RefactoringGuru\State\Conceptual\ConcreteStateB.
ConcreteStateB handles request2.
ConcreteStateB wants to change the state of the context.
Context: Transition to RefactoringGuru\State\Conceptual\ConcreteStateA.