在面向对象软件设计中,经常会遇到依赖于状态的对象。例如,一个 Person(人)对象是依赖于(情绪)状态的。在该对象高兴、郁闷、忧愁和愤怒的时候,行为大不相同;对于同样的外界刺激,反应也不同。这说明同样一个对象,状态不同,其行为也不同。在依赖于状态的对象中,对象的行为依赖于其状态。

与策略模式类似,状态模式(State Pattern)将不同状态下的行为封装在不同的类中,每个类代表一个状态。


Python设计模式(15):状态模式_封装

           


01

状态模式的概念与机制


如果一个对象(类)是依赖于状态的,则程序员在描述该对象的类中,通常会使用许多条件语句来覆盖所有的条件以及在这些条件下的对象的行为。但这种方法对于比较复杂的状态判断容易产生错误。基于大量的条件语句的处理方法有很多不便之处。首先是增加一个新的状态将会导致对类的大量修改,因此该设计不容易扩展和维护;另外,状态转换不明显,也可能发生编程错误。一个有效的处理依赖于状态的设计是利用即将要讲的状态模式。状态模式可以有效地消除在客户程序中的条件语句并且使得状态转换非常清楚。

根据“四人帮”(Gang of Four,GOF)的《设计模式》一书,状态模式是允许一个对象在其内部状态改变时改变他的行为。这个对象看起来似乎修改了它的类。

状态模式将不同状态下的行为封装在一个层次类的不同子类中。下图为状态模式的结构类图。

Python设计模式(15):状态模式_状态模式_02

状态模式的各组成部分及说明如下。

  1. Context:定义了与客户程序的接口,它保持了一个 ConcreteState 的代表现在状态的实例。
  2. State:定义了状态接口,它的各个子类封装了在各种不同状态下的行为。
  3. ConcreteState 子类:封装了在各种不同状态下的行为。

既然上面类图中的每个状态子类都代表一个状态,并且封装了在该状态下的行为 operation(),那么状态的改变到底要哪个类完成呢?事实上,根据实际设计的程序的具体情况,有两种改变状态的方式:一种是状态超类或者各个状态子类负责改变状态,另一种是 Context 类负责改变状态。

在下列情况下可以使用状态模式。

  1. 当对象的行为依赖于状态,对想要在运行时改变状态时使用状态模式。
  2. 当操作带有大量依赖于状态的条件语句时使用该模式。通常许多操作都含有相同的条件结构,状态模式可以将条件结构的每个分支包装成独立分支的类,使得在不同状态下的行为可以独立变化。

关于 Context 类的说明如下。

  1. Context 类代表状态相关的与现有 ConcreteState 有关的请求。
  2. Context 类可以将自己作为一个参数传递给状态对象,以便在需要时状态类可以回访 Context 类。
  3. Context 类是 Client 类的主要接口,Client 类可以用 State 的对象配置 Context 类。当配置好之后,Client 类不必再与 State 类直接交互,与 State 类的交互由 Context 类完成。
  4. Context 或者 State 超类或者子类都可以决定状态转换的顺序。

状态模式的优点如下。

  1. 因为状态相关的代码都被封装在各个状态子类中,所以容易添加新的状态,只需要定义并添加新的状态子类和新的状态变化关系即可。有时其他的类也需要较少的修改。
  2. 将不同的状态封装成不同的类使得状态迁移很明确,并且可以防止 Context 类将状态弄混乱了。

02

关于状态模式的讨论



Python设计模式(15):状态模式_子类_03

关于状态模式的可扩展性的讨论

状态模式的要点如下。

  • Context 类是客户类的主要接口。
  • Context 类代理客户类调用状态类。
  • 在 Context 类保持一个状态类的引用(Reference)。
  • 状态转换主要由状态的超类或者子类负责,也可以由 Context 类负责。
  • 客户类 Client 可以首先创建一个状态子类对象,然后以参数的形式在创建 Context 类的对象的时候,将该子类的对象传递给 Context 类。之后,客户类不与状态类直接交互。

值得注意的是,以上描述没有说明哪个类负责创建状态类的对象,因此该描述是比较粗略的。事实上,由哪个类创建状态类的对象对于可扩展性是非常关键的。现在分别对各种情况进行讨论。

情况 1  标准的设计。在 Context 类中保持状态类 State 的引用;在状态类中也保持一个 Context 类的引用。可以在 Context 类中写一个类似 setStateObj(State s)的方法,然后由状态类(超类或者子类)的某个方法(例如,changeState()方法)根据现在的状态负责创建子类的对象并且调用 Context 类中的 setStateObj(State s)方法,将新创建的状态子类对象赋值给 Context 类中的 State 的引用。在此情况下,在 Context 类中可以没有针对于状态的条件语句。Context 类与 State 类的交互是双方向的,既存在 Context 类对 State 类的调用,又存在从 State 类到 Context 类的调用。导致 Context 类对 State 类有较高的耦合。但是,本设计的可扩展性仍然较好,因为,在添加了一个新的状态类的时候,不必修改 Context 类。

情况 2  层次结构状态模式,即 Client 类调用 Context 类,Context 类调用 State 类,不存在任何相反方向的调用。在状态类中(可以是超类),写一个根据现在的状态创建子类对象的方法 creteStateObj(),然后在 Context 类中负责调用该方法。因为该方法已经是当前状态的子类的对象,所以在 Context 类中,不必使用针对于状态的条件语句。事实上,此时 Context 类根本不知道所使用的对象是哪个状态子类对象,只知道是当前对象。注意,此时,Context 类与 State 类的交互是单方向的:仅存在 Context 类对 State 类的调用,而不存在从 State 类到 Context 类的调用。经过精心设计,可以使得 Client 类仅调用 Context,Context 类仅调用 State 类,而不存在任何其他的调用。该设计有很好的扩展性。

综上所述,在使用状态模式进行设计的时候,可扩展性方面一般达不到开闭原则,但是经过精心的设计,仍然可以大幅度的提高可扩展性。


Python设计模式(15):状态模式_子类_03

策略模式和状态模式的相似之处

两种模式在结构上是相同的,策略模式将每个条件分支封装在一个子类中,而状态模式将每个状态封装在一个子类中。


Python设计模式(15):状态模式_子类_03

策略模式和状态模式的区别

策略模式用来处理具有相同目的但是实现方法不同的算法,这些算法方案之间一般没有状态的变迁,并且用户总是从几个算法中间选取一个。例如,以不同的格式保存文件,以不同的算法压缩文件,以不同的算法排序,以不同的格式绘制同样数据的图形,以不同的方法显示信息,等等。

状态模式则有所不同,它实现的一个概念可以叫做动态继承,也就是继承的(代表一个状态)子类都可以发生变化。状态的变化可以由一个状态迁移图表示。在实现中,如果状态的变化由状态类完成,则在整个变化过程中某些状态之间需要插入一个新的状态,或者状态的顺序之间发生变化,客户程序是根本不用改变的,即对客户是可见的。

一般地说,使用状态模式要比使用策略模式在设计与实现方面会更加复杂一些,原因是用户需要仔细的考虑由谁负责状态转换问题,是由 Context 类负责还是由状态超类负责,还是由状态的子类负责等。

from abc import ABC, abstractmethod


class State(ABC):
@abstractmethod
def operation(self):
pass


class ConcreteStateA(State):
def operation(self):
print('ConcreteStateA')


class ConcreteStateB(State):
def operation(self):
print('ConcreteStateB')


class Context:
def __init__(self, state):
self.state = state

def request(self):
self.state.operation()


class Client:
@staticmethod
def main():
state = ConcreteStateA()
context = Context(state)
context.request()
state = ConcreteStateB()
context.state = state
context.request()


if __name__ == '__main__':
Client.main()

今天的文章有不懂的可以后台回复“加群”,备注:小陈学Python,不备注可是会被拒绝的哦~!

Python设计模式(15):状态模式_封装_06

Python设计模式(15):状态模式_封装_07