Python中的状态模式

64 阅读9分钟

设计模式是对使用面向对象编程的软件开发中常见挑战的常规解决方案。开发可扩展和灵活软件的著名设计模式之一是状态模式。在这篇文章中,你将了解到状态模式以及如何应用它来改善你的软件项目。

有限状态机

在进入状态设计模式之前,让我们先定义一个有限状态机(FSM)。众所周知,状态模式和有限状态机的概念有着密切的关系。有限状态机是一种根据其内部状态而表现出不同行为的东西。在计算机编程中,应用程序中的一个对象的行为根据其状态而变化。一个开关和一个灯泡是FSM的简单例子。"ON "和 "OFF "是灯泡的两种可能状态。要改变灯泡的 "开 "或 "关 "状态,只需拨动开关。过渡是指从一个状态转移到另一个状态的过程。过渡受到几个因素的影响。在灯泡的例子中,它取决于来自开关的输入。下图所示的状态图以图形方式描述了状态和转换。

light bulb state diagram

我们可以使用任何编程语言来实现状态机。根据一些因素,我们的代码会有不同的表现。你可以按以下方式实现前面的灯泡例子。

class LightBulb:
  _state = 'OFF'    # initial state of bulb
  
  def onOff(self, switch):
    if switch == 'ON':
        self._state = 'ON'
    elif switch == 'OFF':
        self._state = 'OFF'
    else:
        continue          # if we get wrong input

对于小的系统,如上面描述的系统,代码似乎是直接和简单的。但是,如果有很多状态和转换,我们的代码就会因为条件语句而变得松散。代码会变得更加广泛,而且不容易维护应用程序。如果你想在程序中增加额外的状态或转换,你就必须改变整个代码库。在这些情况下,你可以使用状态设计模式

状态模式

它是一种行为设计模式。你可以使用状态模式来实现特定状态的行为,其中对象可以在运行时改变其功能。你可以用它来避免在根据对象的状态改变其行为时使用条件语句。在状态模式中,你应该[将](en.wikipedia.org/wiki/Encaps… 类中。原始类根据其当前状态保持对状态对象的引用,而不是使用条件语句来实现依赖于状态的功能。

UML图示

UML class diagram of state pattern

1)Context- 它是我们应用程序的原始类。它保持对其行为所依赖的具体状态之一的引用。它也有一个方法来修改内部状态。

2)状态接口- 所有支持的状态共享相同的状态接口。只有状态接口允许Context与状态对象进行通信。Context只能通过状态接口与状态对象通信。

3)具体状态- 对于每个状态,这些对象实现了 "状态 "接口。这些是主要的对象,包含了特定于状态的方法。

它是如何工作的?

UML sequence diagram of state pattern

假设Context被配置为一个初始状态,concreteStateA 。它的行为与它的初始状态一致。现在Context根据concreteStateA ,实现了doSomething 方法。具体的状态应该包含一个回参考,以回调和改变Context的当前状态对象。如果发生状态转换,Context的 setSate 方法被调用,引用新的状态,concreteStateBContext改变了它的内部状态和行为。现在,它使用concreteStateB 来实现doSomething 方法。其基本思想是,状态可以自动改变上下文的状态。作为一个开发者,你可以通过使用任何数量的setState 的实例来修改状态。

如果你想添加另一个状态,只需创建一个新的具体的状态对象,而无需改变应用程序的上下文

实施

让我们一步步来看看如何实现状态模式。

  1. 找到一个包含状态依赖代码的现有类,或者创建一个合适的上下文类。它应该包括对特定状态的引用,以及在不同状态间切换的方法。
from __future__ import annotations
from abc import ABC, abstractmethod

# the context class contains a _state that references the concrete state and setState method to change between states.
class Context:

    _state = None

    def __init__(self, state: State) -> None:
        self.setState(state)

    def setState(self, state: State):

        print(f"Context: Transitioning to {type(state).__name__}")
        self._state = state
        self._state.context = self

    def doSomething(self):
        self._state.doSomething()
  1. 为所有具体状态创建一个通用的State接口。State接口指定了所有具体状态必须实现的所有方法,以及对Context对象的反向引用。状态可以通过使用这个反向引用将Context改变为另一个状态。
class State(ABC):
    @property
    def context(self) -> Context:
        return self._context

    @context.setter
    def context(self, context: Context) -> None:
        self._context = context

    @abstractmethod
    def doSomething(self) -> None:
        pass

你应该把Context定义为一个受保护的参数。以上。 @property装饰器被用来将 context()方法作为属性,而 @context.setter装饰器将该方法的另一个重载 context()方法的重载作为属性设置方法。现在,_context 是受保护的。

  1. 你可以在实现状态接口的类中定义具体状态。在doSomething 方法被调用后,Context的状态就会改变。你也可以通过定义一个具体的方法来改变状态。状态转换使用ContextsetState 方法。
class ConcreteStateA(State):
    def doSomething(self) -> None:
        print("The context is in the state of ConcreteStateA.")
        print("ConcreteStateA now changes the state of the context.")
        self.context.setState(ConcreteStateB())


class ConcreteStateB(State):
    def doSomething(self) -> None:
        print("The context is in the state of ConcreteStateB.")
        print("ConcreteStateB wants to change the state of the context.")
        self.context.setState(ConcreteStateA())
  1. 现在你可以用一个初始状态启动你的应用程序,并执行这些方法。
# sample application
app = Context(ConcreteStateA())
app.doSomething()    # this method is executed as in state 1
app.doSomething()    # this method is executed as in state 2

上述代码的输出看起来是这样的。

Context: Transitioning to ConcreteStateA
The context is in the state of ConcreteStateA.
ConcreteStateA now changes the state of the context.
Context: Transitioning to ConcreteStateB
The context is in the state of ConcreteStateB.
ConcreteStateB wants to change the state of the context.
Context: Transitioning to ConcreteStateA

例子

让我们创建一个简单的状态机,代表一个真实世界的场景。考虑一个电梯系统,在电梯轿厢里有按钮,可以让你上去或下来。考虑到这个电梯只在两个楼层之间行驶,以保持简单。电梯主要有两种可能的状态。 1st floor2nd floor.两个按钮的输入决定了状态之间的转换。电梯根据其状态执行不同的动作。

下面的代码是电梯例子的实现。请跟随注释了解每个方法的更多描述。

from __future__ import annotations
from abc import ABC, abstractmethod

# The Elevator class is the context. It should be initiated with a default state.
class Elevator:

    _state = None

    def __init__(self, state: State) -> None:
        self.setElevator(state)

    # method to change the state of the object
    def setElevator(self, state: State):

        self._state = state
        self._state.elevator = self

    def presentState(self):
        print(f"Elevator is in {type(self._state).__name__}")

    # the methods for executing the elevator functionality. These depends on the current state of the object.
    def pushDownBtn(self):
        self._state.pushDownBtn()

    def pushUpBtn(self):
        self._state.pushUpBtn()

    # if both the buttons are pushed at a time, nothing should happen
    def pushUpAndDownBtns(self) -> None:
        print("Oops.. you should press one button at a time")

    # if no button was pushed, it should just wait open for guests
    def noBtnPushed(self) -> None:
        print("Press any button. Up or Down")


# The common state interface for all the states
class State(ABC):
    @property
    def elevator(self) -> Elevator:
        return self._elevator

    @elevator.setter
    def elevator(self, elevator: Elevator) -> None:
        self._elevator = elevator

    @abstractmethod
    def pushDownBtn(self) -> None:
        pass

    @abstractmethod
    def pushUpBtn(self) -> None:
        pass


# The concrete states
# We have two states of the elevator: when it is on the First floor and the Second floor
class firstFloor(State):

    # If the down button is pushed when it is already on the first floor, nothing should happen
    def pushDownBtn(self) -> None:
        print("Already in the bottom floor")

    # if up button is pushed, move upwards then it changes its state to second floor.
    def pushUpBtn(self) -> None:
        print("Elevator moving upward one floor.")
        self.elevator.setElevator(secondFloor())


class secondFloor(State):

    # if down button is pushed it should move one floor down and open the door
    def pushDownBtn(self) -> None:
        print("Elevator moving down a floor...")
        self.elevator.setElevator(firstFloor())

    # if up button is pushed nothing should happen
    def pushUpBtn(self) -> None:
        print("Already in the top floor")


if __name__ == "__main__":
    # The client code.

    myElevator = Elevator(firstFloor())
    myElevator.presentState()

    # Up button is pushed
    myElevator.pushUpBtn()

    myElevator.presentState()

上述代码的输出看起来是这样的。

Elevator is in firstFloor
Elevator moving upward one floor.
Elevator is in secondFloor

你可以实现许多按钮和状态的电梯,就像现实生活中的一个楼层。尝试使用状态模式来实现有限状态机中的灯泡例子。

优势和劣势

状态模式,就像其他的编程概念一样,有很多好处,也有一些缺点。通过使用状态模式而不是硬编码特定状态的行为,你可以避免编写大量的条件块在不同的状态间切换。它允许你开发一个灵活和可维护的应用程序。你可以在不改变Context的情况下添加新的状态和转换。

如果每个状态的逻辑很复杂,而且状态经常变化,那么使用状态模式是个好主意。否则,它会使简单的事情复杂化,带来大量的类和对象。状态模式通过强迫客户依赖一个状态对象而增加了另一个层次的间接性,并且它扩展了上下文类,允许状态对象改变上下文的状态。

总结

在这篇文章中,你学会了如何在 Python 编程中使用状态模式来设计状态机。在不使用较大的条件块来实现特定状态的行为的情况下,状态模式使开发过程变得简单了许多。你还可以添加不依赖其他状态的新状态,使你的应用程序更加灵活。状态模式与策略模式非常相似,后者根据用户的选择改变策略。主要的区别在于,具体的状态是知道其他状态的,而策略则不知道。为什么我们说状态意识到其他状态呢?因为每个状态都要知道它们应该移动到哪个状态。例如,一楼的状态知道他们应该换到二楼的状态。与策略模式的另一个重要区别是,在策略模式中,是客户,为Context提供不同的策略,而在状态模式中,状态转换是由Context或State自己管理的。试着在你的软件中使用状态模式,使开发过程更加顺利地进行。