结构型-状态模式

2022-12-04  本文已影响0人  木叶苍蓝

状态模式(State)

意图

状态模式是一种行为设计模式,让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。


532a74f11fdc72bb.png

问题

状态模式与有限状态机的概念紧密相关。


bb0fbc78b89fe30f.png

其主要思想是程序在任意时刻仅可处于几种有限的状态中。在任何一个特定状态中,程序的行为都不相同,且可瞬间从一个状态切换到另一个状态。不过,根据当前状态,程序可能会切换到另外一种状态,也可能会保持当前状态不变。这些数量有限且预先定义的状态切换规则被称为转移。

你还可以将该方法应用在对象上。假如你有一个文档 Document 类。文档可能会处于 草稿 Draft,审阅中 Moderation,已发布 Published 三种状态中的一种。文档的 publish 发布 方法在不同状态下的行为略有不同:

状态机通常由众多条件运算符(ifswitch) 实现,可根据对象的当前状态选择相应的行为。"状态"通常只是对象中的一组成员变量值。即使你之前从未听说过有限状态机,你也很可能已经实现 过状态模式。

class Document is
    field state: string
    // ...
    method publish() is
          switch (state)
              "draft":
                  state = "moderation"
                  break
              "moderation":
                  if (currentUser.rple == "admin")
                      state = "published"
                  break
              "publishe":
                  // 什么也不做
                  break
    // ...

当我们逐步在文档类中添加更多状态和依赖于状态的行为后,基于条件语句的状态机就会暴露器最大的弱点。为了能根据当前状态选择完成相应行为的方法,绝大部分方法中会包含复杂的条件语句。修改器转换逻辑可能会涉及到修改所有方法中的状态条件语句,导致代码的维护工作非常艰难。
这个问题会随着项目进行变得越发严重。我们很难在设计阶段预测到所有可能的状态和转换。随着时间推移,最初仅包含有限条件语句的简洁状态机可能会变成臃肿的一团乱麻。

解决方案

状态模式建议为对象的所有可能状态新建一个类,然后将所有状态的对应行为抽取到这些类中。
原始对象被称为上下文(context),它并不会自行实现所有行为,而是会保存一个指向表示当前状态的状态对象的引用,且将所有与状态相关的工作委派给该对象。

ecdda457121aab1a.png
文档将工作委派给一个状态对象
如需将上下文转换为另外一种状态,则需将当前活动的状态对象替换为另外一个代表新状态的对象。采用这种方式是有前提的:所有状态类都必须遵循同样的接口,而且上下文必须仅通过接口与这些对象进行交互。
这个结构可能看上去与策略模式相似,但又一个关键性 的不同--- 在状态模式中年,特定状态知道其他所有状态的存在,且能触发从一个状态到另一个状态的转换;策略则几乎完全不知道其他策略的存在。

真实世界类比

智能手机的按键和开关会根据设备当前状态完成不同行为:

状态模式结构

c47e6281dfe2fea3.png
  1. 上下文(Context) 保存了对于一个具体状态对象的引用,并会将所有与该状态相关的工作委派给它。上下文通过状态接口与状态对象交互,且会提供一个设置器用于传递新的状态对象。
  2. 状态(State)接口会声明特定与状态的方法。这些方法应能被其他所有具体状态所理解,因为你不希望某些状态所拥有的方法永远不会被调用。
  3. 具体状态(Concrete States) 会自行实现特定与状态的方法。为了避免多个状态中包含相似代码,你可以提供一个封装了部分通用行为的中间抽象类。
  4. 上下文和具体状态都可以设置上下文的下一个状态,并可通过替换连接到上下文的状态对象类完成实际的状态转换。

状态模式适合应用场景

如果对象需要根据自身当前状态进行不同行为,同时状态的数量非常多且与状态相关的代码会频繁变更的话,可使用状态模式。
模式建议你将所有特定与状态的代码抽取到一组独立的类中。这样一来,你可以在独立于其他状态的情况下添加新的状态或修改已有状态,从而减少维护成本。
如果某个类 需要根据成员变量的当前值改变自身行为,从而需要使用大量的条件语句时,可使用该模式。
状态模式会将这些条件语句的分支抽取到相应状态类的方法中。同时,你还可以清除主要类中与特定状态相关的临时成员变量和帮手方法代码。
当相似类型和基于条件的状态机转换中存在许多重复代码,可使用状态模式。
状态模式让你能够生成状态类层次结构,通过将公用代码抽取到抽象基类中减少重复。

实现方式

  1. 确定哪些类是上下文。它可能是包含依赖于状态的代码的已有类;如果特定与状态 的代码分散在多个类中,那么它可能是一个新的类。
  2. 声明状态接口。虽然你可能会需要完全复制上下文件声明的所有方法,但是最好是仅把关注点放在哪些可能包含特定于状态的行为的方法上。
  3. 为每一个实际状态创建一个继承于状态接口的类。然后检查上下文中的方法并将与特定状态相关的所有代码抽取到新建的类中。在将代码移动到状态类的过程中,你可能会发现它依赖于上下文中的一些私有成员。你可以采用以下几种变通的方式:
    • 将这些成员变量或方法设为公有。
    • 将需要抽取的上下文行为更改为上下文中的公有方法,然后在状态类中调用。这种方式简陋却便捷,你可以稍后再对其进行修补。
    • 将状态类嵌套在上下文件类中。这种方式需要你所使用的编程语言支持嵌套类。
  4. 在上下文类中添加一个状态接口类型的引用成员变量,以及一个用于修改该成员变量值的公有设置器。
  5. 为切换上下文状态,你需要创建某个状态类实例并将其传递给上下文。你可以在上下文,各种状态或客户端中完成这项工作。无论在何处完成这项工作,该类都将依赖于其所实例化的具体类。

状态模式缺点及适用场景

与其他模式的关系

使用示例:在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子类实例的引用,它表示当前上下文的状态。
    """
    _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.
        Context允许在运行时更改State对象。
        """
        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.
    Context将其部分行为委托给当前State对象。
    """
    def reqyest1(self):
        self._state.handle1()

    def request2(self):
        self._state.handle2()

class State(ABS):
    """
    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.
    基State类声明所有Concrete 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 handler 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 handler2(self) -> None:
        print("ConcreteStateB handles request2.")
        print("ConcreteStateB wants to change the state of the context.")
        self.context.transition_to(ConcreteStateA())

if __name__ == "__main__":
    context = Context(ConcreteStateA())
    context.request1()
    context.request2()

Output.txt 执行结果

Context: Trancstition 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: Transtition to ConcreteStateA

真实示例

例一:电梯控制器

电梯在我们周边随处可见,电梯的控制逻辑中心是由电梯控制器实现的。电梯的控制逻辑,即使简单点设计,把状态分成开门状态,停止状态,运行状态。操作分成开门,关门,运行,停止。那流程也是很复杂的。首先,开门状态不能开门、运行、停止;停止状态不能关门、停止。运行状态不能开门、关门、运行。要用一个个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):
        return "RUN: Close Forbidden."
        return self

    def run(self):
        print "Run: Close Forbidden."
        return self

    def stop(self):
        print "RUN: The lift start to stop..."
        print "RUN:The lift stoppend..."
        return StopState()

class StopState(LifiState):
    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_statue

    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,被禁止,然后,关门、运行、停止。

例二:电视机开机,关机
# -*- 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):
        return 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()
例三:电脑开机,待机,关机
# -*- 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
"""

https://www.cnblogs.com/amgulen/p/16650186.html 大家也可以看原著,感谢原著作者的分享。

上一篇下一篇

猜你喜欢

热点阅读