0%

译-设计模式-行为模式之State

意图

State是行为模式的一种,它允许你在对象内部状态发生变化时改变其行为。这个对象会改变它的类。

问题

状态模式和有限状态机很相似。

它主要的思想是程序是几个状态之一,这些状态相互关联。状态的数量和它们之间的转换是有限的并且是预先定义的。根据当前的状态,程序对相同的事件会有不同的响应。

类似的方法可以应用到对象上。比如,Document(提案)对象可以是以下三种状态之一:Draft(草案),Moderations(待审)和Publish(发布)。对每种状态而言,publish方法会有不同的处理方式:

  • 第一种状态,它将改变提案的状态到待审。

  • 第二种状态,它将使提案变成发布状态,当然只有在当前用户是管理员时。

  • 第三种状态,它什么也不做。

状态机通常基于许多条件操作来实现,比如,if或者wsitch来检查当前状态并作出适当的行为。即使你第一次听说状态机,你很可能已经至少实现多一次。下面这段代码看起来是不是很眼熟?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Document
string state;
// ...
method publish() {
switch (state) {
"draft":
state = "moderation";
break;
"moderation":
if (currentUser.role == 'admin')
state = "published"
break;
"published":
// Do nothing.
}
}
// ...

当你你要添加更多状态或者状态依赖行为到Document中时,使用条件构建起来的状态机最大的限制就展露出来。大多方法需要有很多条件参数来决定这个状态下的正确行为。

这些代码很难维护,因为任何转换逻辑的改变都需要在每个方法中对每个条件做双重检查。

项目越老这个问题往往越大,因为在设计阶段事先预测所有可能的状态和转换是相当困难的。因此,一个由少量条件构成的精简状态机随着时间的推移会变的一团糟。

解决

状态模式建议为上下文对象的所有可能状态创建新类,并将与状态有关的行为提取到这些类中。

上下文将会包含一个代表当前状态的状态对象。上下文将把执行交给状态对象,而不是自己去处理。

为了改变上下文对象,一个状态对象可以将另一个状态对象传到上下文中。但是为了让状态可以呼唤,所有的状态类必须遵循相同的接口,并且上下文必须通过状态对象的接口和它通信。

上面描述的结构看起来有些像Strategy模式,但是它们有一个关键点不同。在状态模式中,上下文和特定的状态都可以发起状态间的转换。

真实世界类比

智能手机

智能手机当前处于不同的状态会有不同的行为:

  • 当手机处于解锁状态,按下按钮会执行一些列功能。

  • 当手机被锁定,所有的按钮都会展示解锁画面。

  • 当手机电量低,所有按钮都会展示充电画面。

结构

state.png

  1. Context(上下文)持有Concrete State(具体状态)对象的引用,并把状态相关的行为委托给它。上下文通过状态通用的接口和状态对象协作。上下文必须报漏出一个方法来接收一个新状态对象。

  2. State(状态)为具体状态声明通用的接口。它声明的方法应当对所有的状态都是有意义的。但是每个状态应该提供不同的实现。

  3. Concrete State实现特定状态的行为。增加状态基类避免在多个状态中出现重复代码。

    一个状态可以持有上下文的引用。这不仅让它可以访问上下文数据,也提供了一种法器状态转变的方式。

  4. 上下文和具体的状态都可以决定何时发起状态转换及决定转换到什么状态。为了处理转换,一个新的状态对象应当被传给上下文。

伪代码

在这个例子中,状态模式依赖当前不同的播放器的状态来控制播放器不同的行为。Player主类持有一个状态对象的引用,它被用来处理播放器的大多数工作。用户的一些操作会使播放器转换到另一个状态。

状态模式让播放器改变其行为而对其他对象无感知。转换可以被播放器本身或者特定的状态对象执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// Common interface for all states.
abstract class State is
protected field player: Player

// Context passes itself through the state constructor.
// This may help a state to fetch some useful context
// data if needed.
constructor State(player) is
this.player = player

abstract method clickLock()
abstract method clickPlay()
abstract method clickNext()
abstract method clickPrevious()


// Concrete states provide the special implementation for
// all interface methods.
class LockedState is

// When you unlock a locked player, it may assume one of
// the two states.
method clickLock() is
if (player.playing)
player.changeState(new PlayingState(player))
else
player.changeState(new ReadyState(player))

method clickPlay() is
Do nothing.

method clickNext() is
Do nothing.

method clickPrevious() is
Do nothing.


// They can also trigger state transitions in the context.
class ReadyState 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 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)


// Player acts as a context.
class Player is
field state: State
field UI, volume, playlist, currentSong

constructor Player() is
this.state = new ReadyState(this)

// Context delegates handling user's input to a
// state object. Naturally, the outcome will depend
// on what state is currently active, since all
// states 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 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()

// 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
// ...

适用性

  • 当你有一个对象在不同状态下会有不同行为时。状态的数量有很多。状态关联的代码经常改变。

    状态模式建议隔离那些和状态相关的代码到不同的类中。源类被叫做“上下文”,它要持有这些状态对象中的其中一个。它应当把工作委托给这个状态对象。这种结构允许通过提供给上下文不同状态对象的方式改变上下文的行为。

  • 当一个类被大量的通过当前类字段值来改变方法行为的条件污染。

    状态模式把条件分支转换成合适状态类中的方法。然后需要依赖多态吧执行委托到关联的状态对象。

  • 在基于条件的状态机中有大量重复代码分布在相似状态和转换中时。

    状态模式允许你组合状态类的层次,通过把通用的代码移动到层级的基类中来减少重复。

如何实现

  1. 声明那个类来扮演Context角色。这可能是一个存在的类,已经有了状态依赖的相关代码;或者是一个新类,如果状态相关的代码分布在各个类中。

  2. 创建State接口。大多情况下,他将镜像声明在Context中的方法。

  3. 为每个真正的状态,创建一个State接口的实现。遍历Context的方法,把所有和状态有关的代码放到对应的状态类中。

  4. 添加一个State类型的引用字段到Context类中,并且需要提供一个可复写这个字段值的public方法。

  5. 再一次遍历Context中的方法,用状态对象的相应方法替换剩余的状态条件部分。

  6. 为了转换Context的状态,必须创建一个状态实例传给上下文。这步可有Context自己,或者通过State,或者Client来完成。注意,创建者将需要依赖具体的State类进行实例化。

优点

  • 消除状态机条件。

  • 把与特定状态相关的代码组织到不同的类中。

  • 简化代码上下文。

缺点

  • 如果一个状态机只有少数几个状态或很少发生变化,那么这可能是过度设计。

和其他模式的关系

  • State,Strategy,Bridge(某种意义上的Adapter)有相似的解决结构。他们都共享“handle/body”元素。他们的意图有所不同 - 也就是说,他们解决不同的问题。

  • State可以看作是Strategy模式的扩展。这两种模式都使用组合把工作委托给辅助对象来改变主对象的行为。但是,State模式允许状态对象用另一个状态改变当前上下文的状态,使得它们相互依赖。

小结

State是一种行为设计模式,允许对象在其内部状态改变时改变行为。该模式将与状态相关的行为提取为单独的状态类,并强制原始对象将工作委派给状态类的实例,而不是自行处理。

参考

翻译整理自:https://refactoring.guru/design-patterns/state