2.状态模式(State)
状态模式(State)
意图
状态模式是一种行为设计模式, 让你能在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。
问题
状态模式与有限状态机 的概念紧密相关。
其主要思想是程序在任意时刻仅可处于几种有限的状态中。 在任何一个特定状态中, 程序的行为都不相同, 且可瞬间从一个状态切换到另一个状态。 不过, 根据当前状态, 程序可能会切换到另外一种状态, 也可能会保持当前状态不变。 这些数量有限且预先定义的状态切换规则被称为转移。
你还可将该方法应用在对象上。 假如你有一个 文档 Document
类。 文档可能会处于 草稿 Draft
、 审阅中 Moderation
和 已发布 Published
三种状态中的一种。 文档的 publish 发布
方法在不同状态下的行为略有不同:
- 处于
草稿
状态时, 它会将文档转移到审阅中状态。 - 处于
审阅中
状态时, 如果当前用户是管理员, 它会公开发布文档。 - 处于
已发布
状态时, 它不会进行任何操作。
状态机通常由众多条件运算符 ( 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":
// 什么也不做。
break
// ……
当我们逐步在 文档
类中添加更多状态和依赖于状态的行为后, 基于条件语句的状态机就会暴露其最大的弱点。 为了能根据当前状态选择完成相应行为的方法, 绝大部分方法中会包含复杂的条件语句。 修改其转换逻辑可能会涉及到修改所有方法中的状态条件语句, 导致代码的维护工作非常艰难。
这个问题会随着项目进行变得越发严重。 我们很难在设计阶段预测到所有可能的状态和转换。 随着时间推移, 最初仅包含有限条件语句的简洁状态机可能会变成臃肿的一团乱麻。
解决方案
状态模式建议为对象的所有可能状态新建一个类, 然后将所有状态的对应行为抽取到这些类中。
原始对象被称为上下文 (context
), 它并不会自行实现所有行为, 而是会保存一个指向表示当前状态的状态对象的引用, 且将所有与状态相关的工作委派给该对象。
文档将工作委派给一个状态对象。
如需将上下文转换为另外一种状态, 则需将当前活动的状态对象替换为另外一个代表新状态的对象。 采用这种方式是有前提的: 所有状态类都必须遵循同样的接口, 而且上下文必须仅通过接口与这些对象进行交互。
这个结构可能看上去与策略模式相似, 但有一个关键性的不同——在状态模式中, 特定状态知道其他所有状态的存在, 且能触发从一个状态到另一个状态的转换; 策略则几乎完全不知道其他策略的存在。
真实世界类比
智能手机的按键和开关会根据设备当前状态完成不同行为:
- 当手机处于解锁状态时, 按下按键将执行各种功能。
- 当手机处于锁定状态时, 按下任何按键都将解锁屏幕。
- 当手机电量不足时, 按下任何按键都将显示充电页面。
状态模式结构
-
上下文 (
Context
) 保存了对于一个具体状态对象的引用, 并会将所有与该状态相关的工作委派给它。 上下文通过状态接口与状态对象交互, 且会提供一个设置器用于传递新的状态对象。 -
状态 (
State
) 接口会声明特定于状态的方法。 这些方法应能被其他所有具体状态所理解, 因为你不希望某些状态所拥有的方法永远不会被调用。 -
具体状态 (
Concrete States
) 会自行实现特定于状态的方法。 为了避免多个状态中包含相似代码, 你可以提供一个封装有部分通用行为的中间抽象类。状态对象可存储对于上下文对象的反向引用。 状态可以通过该引用从上下文处获取所需信息, 并且能触发状态转移。
-
上下文和具体状态都可以设置上下文的下个状态, 并可通过替换连接到上下文的状态对象来完成实际的状态转换。
状态模式适合应用场景
如果对象需要根据自身当前状态进行不同行为, 同时状态的数量非常多且与状态相关的代码会频繁变更的话, 可使用状态模式。
模式建议你将所有特定于状态的代码抽取到一组独立的类中。 这样一来, 你可以在独立于其他状态的情况下添加新状态或修改已有状态, 从而减少维护成本。
如果某个类需要根据成员变量的当前值改变自身行为, 从而需要使用大量的条件语句时, 可使用该模式。
状态模式会将这些条件语句的分支抽取到相应状态类的方法中。 同时, 你还可以清除主要类中与特定状态相关的临时成员变量和帮手方法代码。
当相似状态和基于条件的状态机转换中存在许多重复代码时, 可使用状态模式。
状态模式让你能够生成状态类层次结构, 通过将公用代码抽取到抽象基类中来减少重复。
实现方式
-
确定哪些类是上下文。 它可能是包含依赖于状态的代码的已有类; 如果特定于状态的代码分散在多个类中, 那么它可能是一个新的类。
-
声明状态接口。 虽然你可能会需要完全复制上下文中声明的所有方法, 但最好是仅把关注点放在那些可能包含特定于状态的行为的方法上。
-
为每个实际状态创建一个继承于状态接口的类。 然后检查上下文中的方法并将与特定状态相关的所有代码抽取到新建的类中。
在将代码移动到状态类的过程中, 你可能会发现它依赖于上下文中的一些私有成员。 你可以采用以下几种变通方式:
- 将这些成员变量或方法设为公有。
- 将需要抽取的上下文行为更改为上下文中的公有方法, 然后在状态类中调用。 这种方式简陋却便捷, 你可以稍后再对其进行修补。
- 将状态类嵌套在上下文类中。 这种方式需要你所使用的编程语言支持嵌套类。
-
在上下文类中添加一个状态接口类型的引用成员变量, 以及一个用于修改该成员变量值的公有设置器。
-
再次检查上下文中的方法, 将空的条件语句替换为相应的状态对象方法。
-
为切换上下文状态, 你需要创建某个状态类实例并将其传递给上下文。 你可以在上下文、 各种状态或客户端中完成这项工作。 无论在何处完成这项工作, 该类都将依赖于其所实例化的具体类。
状态模式优缺点及适用场景
- 单一职责原则。 将与特定状态相关的代码放在单独的类中。
- 开闭原则。 无需修改已有状态类和上下文就能引入新状态。
- 通过消除臃肿的状态机条件语句简化上下文代码。结构清晰,相比于if…else…简约了不少。
- 如果状态机只有很少的几个状态, 或者很少发生改变, 那么应用该模式可能会显得小题大作。
- 在状态比较多时,子类也会非常多,不便于管理。
- 行为状态改变的场景。这点在各种控制器中非常常见,同时,逻辑结构为状态转移图的场景中都非常适用。
与其他模式的关系
- 桥接模式、 状态模式和策略模式 (在某种程度上包括适配器模式) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。
- 状态可被视为策略的扩展。 两者都基于组合机制: 它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。 策略使得这些对象相互之间完全独立, 它们不知道其他对象的存在。 但状态模式没有限制具体状态之间的依赖, 且允许它们自行改变在不同情景下的状态。
使用示例: 在 Python 语言中, 状态模式通常被用于将基于 switch
语句的大型状态机转换为对象。
识别方法: 状态模式可通过受外部控制且能根据对象状态改变行为的方法来识别。
概念示例
本例说明了状态设计模式的结构并重点回答了下面的问题:
- 它由哪些类组成?
- 这些类扮演了哪些角色?
- 模式中的各个元素会以何种方式相互关联?
main.py
概念示例
from __future__ import annotations
from abc import ABC, abstractmethod
class Context:
"""
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.
"""
_state = None
"""
A reference to the current state of the Context.
"""
def __init__(self, state: State) -> None:
self.transition_to(state)
def transition_to(self, state: State):
"""
The Context allows changing the State object at runtime.
"""
print(f"Context: Transition to {type(state).__name__}")
self._state = state
self._state.context = self
"""
The Context delegates part of its behavior to the current State object.
"""
def request1(self):
self._state.handle1()
def request2(self):
self._state.handle2()
class State(ABC):
"""
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.
"""
@property
def context(self) -> Context:
return self._context
@context.setter
def context(self, context: Context) -> None:
self._context = context
@abstractmethod
def handle1(self) -> None:
pass
@abstractmethod
def handle2(self) -> None:
pass
"""
Concrete States implement various behaviors, associated with a state of the
Context.
"""
class ConcreteStateA(State):
def handle1(self) -> None:
print("ConcreteStateA handles request1.")
print("ConcreteStateA wants to change the state of the context.")
self.context.transition_to(ConcreteStateB())
def handle2(self) -> None:
print("ConcreteStateA handles request2.")
class ConcreteStateB(State):
def handle1(self) -> None:
print("ConcreteStateB handles request1.")
def handle2(self) -> None:
print("ConcreteStateB handles request2.")
print("ConcreteStateB wants to change the state of the context.")
self.context.transition_to(ConcreteStateA())
if __name__ == "__main__":
# The client code.
context = Context(ConcreteStateA())
context.request1()
context.request2()
Output.txt
: 执行结果
Context: Transition to ConcreteStateA
ConcreteStateA handles request1.
ConcreteStateA wants to change the state of the context.
Context: Transition to ConcreteStateB
ConcreteStateB handles request2.
ConcreteStateB wants to change the state of the context.
Context: Transition to ConcreteStateA
真实示例
例1:电梯控制器
电梯在我们周边随处可见,电梯的控制逻辑中心是由电梯控制器实现的。电梯的控制逻辑,即使简单点设计,把状态分成开门状态,停止状态和运行状态,操作分成开门、关门、运行、停止,那流程也是很复杂的。首先,开门状态不能开门、运行、停止;停止状态不能关门,停止;运行状态不能开门、关门、运行。要用一个一个if…else…实现,首先代码混乱,不易维护;二是不易扩展。至于各种设计原则什么的……
那该如何实现?在上边的逻辑中,每个操作仅仅是一个操作,状态切换与操作是分离的,这也造成后来操作和状态“相互配合”的“手忙脚乱”。如果把状态抽象成一个类,每个状态为一个子类,每个状态实现什么操作,不实现什么操作,仅仅在这个类中具体实现就可以了。
下面我们实现这个逻辑。
先实现抽象的状态类:
class LiftState:
def open(self):
pass
def close(self):
pass
def run(self):
pass
def stop(self):
pass
而后实现各个具体的状态类:
class OpenState(LiftState):
def open(self):
print "OPEN:The door is opened..."
return self
def close(self):
print "OPEN:The door start to close..."
print "OPEN:The door is closed"
return StopState()
def run(self):
print "OPEN:Run Forbidden."
return self
def stop(self):
print "OPEN:Stop Forbidden."
return self
class RunState(LiftState):
def open(self):
print "RUN:Open Forbidden."
return self
def close(self):
print "RUN:Close Forbidden."
return self
def run(self):
print "RUN:The lift is running..."
return self
def stop(self):
print "RUN:The lift start to stop..."
print "RUN:The lift stopped..."
return StopState()
class StopState(LiftState):
def open(self):
print "STOP:The door is opening..."
print "STOP:The door is opened..."
return OpenState()
def close(self):
print "STOP:Close Forbidden"
return self
def run(self):
print "STOP:The lift start to run..."
return RunState()
def stop(self):
print "STOP:The lift is stopped."
return self
为在业务中调度状态转移,还需要将上下文进行记录,需要一个上下文的类。
class Context:
lift_state=""
def getState(self):
return self.lift_state
def setState(self,lift_state):
self.lift_state=lift_state
def open(self):
self.setState(self.lift_state.open())
def close(self):
self.setState(self.lift_state.close())
def run(self):
self.setState(self.lift_state.run())
def stop(self):
self.setState(self.lift_state.stop())
这样,在进行电梯的调度时,只需要调度Context就可以了。业务逻辑中如下所示:
if __name__=="__main__":
ctx = Context()
ctx.setState(StopState())
ctx.open()
ctx.run()
ctx.close()
ctx.run()
ctx.stop()
打印如下:
STOP:The door is opening...
STOP:The door is opened...
OPEN:Run Forbidden.
OPEN:The door start to close...
OPEN:The dorr is closed
STOP:The lift start to run...
RUN:The lift start to stop...
RUN:The lift stopped...
由逻辑中可知,电梯先在STOP状态,然后开门,开门时运行Run,被禁止,然后,关门、运行、停止。
例2:电视机开机,关机
# -*- coding: utf-8 -*-
from abc import ABCMeta, abstractmethod
class State(metaclass=ABCMeta):
@abstractmethod
def do_this(self):
pass
class StartState(State):
def do_this(self):
print("start state")
class StopState(State):
def do_this(self):
print("stop state")
class TVContext(State):
def __init__(self):
self.state = None
def get_state(self):
return self.state
def set_state(self, state):
self.state = state
def do_this(self):
self.state.do_this()
if __name__ == '__main__':
context = TVContext()
start = StartState()
stop = StopState()
context.set_state(start)
context.do_this()
context.set_state(stop)
context.do_this()
"""
start state
stop state
"""
例3:电脑开机,待机,关机
# -*- coding: utf-8 -*-
# 接口
class ComputerState(object):
name = "state"
allowed = []
def switch(self, state):
if state.name in self.allowed:
print("switch state: {}".format(state.name))
self.__class__ = state
else:
print("state not allowed")
def __str__(self):
return self.name
# 开机
class On(ComputerState):
name = "on"
allowed = ["off", "hibernate"]
# 关机
class Off(ComputerState):
name = "off"
allowed = ["on"]
# 休眠
class Hibernate(ComputerState):
name = "hibernate"
allowed = ["on", "off"]
# 客户端
class Computer(object):
def __init__(self):
self.state = Off() # 初始状态是关机状态
def change(self, state):
self.state.switch(state)
if __name__ == '__main__':
computer = Computer()
# 开机 -> 休眠 -> 关机
computer.change(On)
computer.change(Hibernate)
computer.change(Off)
# 关机的请情况下休眠
computer.change(Hibernate)
"""
switch state: on
switch state: hibernate
switch state: off
state not allowed
"""