Python設計模式之狀態模式(14)

狀態模式(The State Pattern):實現有限的狀態機,類的行爲是基於它的狀態改變的。是一種行爲設計模式, 讓你能在一個對象的內部狀態變化時改變其行爲, 使其看上去就像改變了自身所屬的類一樣。
在狀態模式中,我們創建表示各種狀態的對象和一個行爲隨着狀態對象改變而改變的 context 對象。
其主要思想是程序在任意時刻僅可處於幾種有限的狀態中。 在任何一個特定狀態中, 程序的行爲都不相同, 且可瞬間從一個狀態切換到另一個狀態。 不過, 根據當前狀態, 程序可能會切換到另外一種狀態, 也可能會保持當前狀態不變。 這些數量有限且預先定義的狀態切換規則被稱爲轉移。

1 介紹

  • 意圖:允許對象在內部狀態發生改變時改變它的行爲,對象看起來好像修改了它的類。
  • 主要解決:對象的行爲依賴於它的狀態(屬性),並且可以根據它的狀態改變而改變它的相關行爲。
  • 何時使用:代碼中包含大量與對象狀態有關的條件語句。
    如何解決:將各種具體的狀態類抽象出來。
  • 關鍵代碼:通常命令模式的接口中只有一個方法。而狀態模式的接口中有一個或者多個方法。而且,狀態模式的實現類的方法,一般返回值,或者是改變實例變量的值。也就是說,狀態模式一般和對象的狀態有關。實現類的方法有不同的功能,覆蓋接口中的方法。狀態模式和命令模式一樣,也可以用於消除 if…else 等條件選擇語句。
  • 優點: 1、 封裝了轉換規則。 2、枚舉可能的狀態,在枚舉狀態之前需要確定狀態種類。 3、將所有與某個狀態有關的行爲放到一個類中,並且可以方便地增加新的狀態,只需要改變對象狀態即可改變對象的行爲。 4、允許狀態轉換邏輯與狀態對象合成一體,而不是某一個巨大的條件語句塊。 5、可以讓多個環境對象共享一個狀態對象,從而減少系統中對象的個數。
  • 缺點: 1、狀態模式的使用必然會增加系統類和對象的個數。 2、狀態模式的結構與實現都較爲複雜,如果使用不當將導致程序結構和代碼的混亂。 3、狀態模式對"開閉原則"的支持並不太好,對於可以切換狀態的狀態模式,增加新的狀態類需要修改那些負責狀態轉換的源代碼,否則無法切換到新增狀態,而且修改某個狀態類的行爲也需修改對應類的源代碼。
  • 使用場景: 1、行爲隨狀態改變而改變的場景。 2、條件、分支語句的代替者。
  • 注意事項:在行爲受狀態約束的時候使用狀態模式,而且狀態不超過 5 個

2 適用場景

  • 如果對象需要根據自身當前狀態進行不同行爲, 同時狀態的數量非常多且與狀態相關的代碼會頻繁變更的話, 可使用狀態模式。
    模式建議你將所有特定於狀態的代碼抽取到一組獨立的類中。 這樣一來, 你可以在獨立於其他狀態的情況下添加新狀態或修改已有狀態, 從而減少維護成本。
  • 如果某個類需要根據成員變量的當前值改變自身行爲, 從而需要使用大量的條件語句時, 可使用該模式。
    狀態模式會將這些條件語句的分支抽取到相應狀態類的方法中。 同時, 你還可以清除主要類中與特定狀態相關的臨時成員變量和幫手方法代碼。
  • 當相似狀態和基於條件的狀態機轉換中存在許多重複代碼時, 可使用狀態模式。
    狀態模式讓你能夠生成狀態類層次結構, 通過將公用代碼抽取到抽象基類中來減少重複。

3 使用步驟

狀態設計模式通常使用一個父State類和許多派生的ConcreteState類來實現,父類包含所有狀態共同的功能,每個派生類則僅包含特定狀態要求的功能。狀態模式關注的是實現一個狀態機,狀態機的核心部分是狀態和狀態之間的轉換。每個部分具體如何實現並不重要。

假如你有一個 文檔Document類。 文檔可能會處於 草稿Draft 、 ​ 審閱中Moderation和 已發佈Published三種狀態中的一種。 文檔的 publish發佈方法在不同狀態下的行爲略有不同:

  • 處於 草稿狀態時, 它會將文檔轉移到審閱中狀態。
  • 處於 審閱中狀態時, 如果當前用戶是管理員, 它會公開發布文檔。
  • 處於 已發佈狀態時,它不會進行任何操作。
    在這裏插入圖片描述
    狀態模式建議爲對象的所有可能狀態新建一個類, 然後將所有狀態的對應行爲抽取到這些類中。
    原始對象被稱爲上下文 (context), 它並不會自行實現所有行爲, 而是會保存一個指向表示當前狀態的狀態對象的引用, 且將所有與狀態相關的工作委派給該對象。

在這裏插入圖片描述
如需將上下文轉換爲另外一種狀態, 則需將當前活動的狀態對象替換爲另外一個代表新狀態的對象。 採用這種方式是有前提的: 所有狀態類都必須遵循同樣的接口, 而且上下文必須僅通過接口與這些對象進行交互。
這個結構可能看上去與策略模式相似, 但有一個關鍵性的不同——在狀態模式中, 特定狀態知道其他所有狀態的存在, 且能觸發從一個狀態到另一個狀態的轉換; 策略則幾乎完全不知道其他策略的存在。

狀態模式結構
模式結構
實現方式

(1) 確定哪些類是上下文。 它可能是包含依賴於狀態的代碼的已有類; 如果特定於狀態的代碼分散在多個類中, 那麼它可能是一個新的類。
(2)聲明狀態接口。 雖然你可能會需要完全複製上下文中聲明的所有方法, 但最好是僅把關注點放在那些可能包含特定於狀態的行爲的方法上。
(3)爲每個實際狀態創建一個繼承於狀態接口的類。 然後檢查上下文中的方法並將與特定狀態相關的所有代碼抽取到新建的類中。
在將代碼移動到狀態類的過程中, 你可能會發現它依賴於上下文中的一些私有成員。 你可以採用以下幾種變通方式:

  • 將這些成員變量或方法設爲公有。
  • 將需要抽取的上下文行爲更改爲上下文中的公有方法, 然後在狀態類中調用。 這種方式簡陋卻便捷, 你可以稍後再對其進行修補。
  • 將狀態類嵌套在上下文類中。 這種方式需要你所使用的編程語言支持嵌套類。

(5)在上下文類中添加一個狀態接口類型的引用成員變量, 以及一個用於修改該成員變量值的公有設置器。
(6)再次檢查上下文中的方法, 將空的條件語句替換爲相應的狀態對象方法。
(7) 爲切換上下文狀態, 你需要創建某個狀態類實例並將其傳遞給上下文。 你可以在上下文、 各種狀態或客戶端中完成這項工作。 無論在何處完成這項工作, 該類都將依賴於其所實例化的具體類。

4 代碼示例

概念示例

from abc import ABC, abstractmethod

class Context(ABC):
    """
    Contex類,是client端調用的入口。該類需要接收State對象,指示當前State狀態的上下文,並且維護各種State對象的引用
    """
    _state = None

    def __init__(self, state):
        """接收state對象,並且指向該對象"""
        self.transition_to(state)

    def transition_to(self, state):
        """程序運行中切換state對象, 將輸入對象的上下文切換爲當前state對象"""
        print(f"Context: Transition to {type(state).__name__}")
        self._state = state
        self._state.context = self

    ##當前對象的行爲
    def request1(self):
        self._state.handle1()

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

class State(ABC):
    """State抽象類,聲明各個子類需要實現的方法。並且提供到Context對象的回溯引用,該引用可用來根據context上下文切換到指定的state"""
    @property
    def context(self):
        return self._context

    @context.setter
    def context(self, context):
        self._context = context

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

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


class StateA(State):
    def handle1(self) -> None:
        print(f"{type(self).__name__} handle request1")
        print(f"{type(self).__name__} wants to change the state of the context.")
        self.context.transition_to(StateB())

    def handle2(self) -> None:
        print(f"{type(self).__name__} handle request2")

class StateB(State):
    def handle1(self) -> None:
        print(f"{type(self).__name__} handle request1")
        
    def handle2(self) -> None:
        print(f"{type(self).__name__} handle request2")
        print(f"{type(self).__name__} wants to change the state of the context.")
        self.context.transition_to(StateA())


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

運行結果:

Context: Transition to StateA
StateA handle request1
StateA wants to change the state of the context.
Context: Transition to StateB
StateB handle request2
StateB wants to change the state of the context.
Context: Transition to StateA

案例1:
電梯狀態流轉控制,電池在開啓狀態時,能關閉;運行中時,能停止;停止狀態時,可以close。

from abc import ABC, abstractmethod

class LiftState(ABC):
    """
    State抽象類,聲明子類必須實現的方法
    """
    @abstractmethod
    def open(self):
        pass

    @abstractmethod
    def close(self):
        pass

    @abstractmethod
    def run(self):
        pass

    @abstractmethod
    def stop(self):
        pass



#實現具體的狀態流轉控制
class OpenState(LiftState):

    def open(self):
        print(f"{type(self).__name__}: The door is opened...")
        return self

    def close(self):
        print(f"{type(self).__name__}: The door start to close")
        print(f"{type(self).__name__}: The door closed, will run")
        return RunState()

    def run(self):
        print(f"{type(self).__name__}: Run Forbidden ")
        return self

    def stop(self):
        print(f"{type(self).__name__}: Stop Forbidden ")
        return self

class CloseState(LiftState):

    def open(self):
        print(f"{type(self).__name__}: The door start to open")
        print(f"{type(self).__name__}: The door opened")
        return OpenState()

    def close(self):
        print(f"{type(self).__name__}: The door is already closed")
        return self

    def run(self):
        print(f"{type(self).__name__}: The door start to run")
        print(f"{type(self).__name__}: The door is running")
        return RunState()

    def stop(self):
        print(f"{type(self).__name__}: Stop Forbidden ")
        return self


class RunState(LiftState):

    def open(self):
        print(f"{type(self).__name__}: Open Forbidden")
        return self

    def close(self):
        print(f"{type(self).__name__}: Close Forbidden ")
        return RunState()

    def run(self):
        print(f"{type(self).__name__}: lift is running ")
        return self

    def stop(self):
        print(f"{type(self).__name__}: Start to stop ")
        print(f"{type(self).__name__}: lift Stoped ")
        return StopState()


class StopState(LiftState):

    def open(self):
        print(f"{type(self).__name__}: Start to open ")
        print(f"{type(self).__name__}: lift is opened ")
        return OpenState()

    def close(self):
        print(f"{type(self).__name__}: Close Forbidden ")
        return self

    def run(self):
        print(f"{type(self).__name__}: Run Forbidden ")
        return self

    def stop(self):
        print(f"{type(self).__name__}: Run Forbidden ")
        return self

##記錄狀態流轉上下文,控制流轉調度
class Context(object):
    _lift_state = None

    def __init__(self, state):
        self.transition_to(state)

    def get_state(self):
        return self._lift_state

    def transition_to(self, state):
        """程序運行中切換state對象, 將輸入對象的上下文切換爲當前state對象"""
        print(f"Context: Transition to {type(state).__name__}")
        self._lift_state = state
        self._lift_state.context = self

    def open(self):
        self.transition_to(self._lift_state.open())

    def close(self):
        self.transition_to(self._lift_state.close())

    def run(self):
        self.transition_to(self._lift_state.run())

    def stop(self):
        self.transition_to(self._lift_state.stop())

if __name__ == "__main__":
    ctx = Context(StopState())
    ctx.open()
    ctx.close()
    ctx.run()
    ctx.stop()
    ctx.run()

運行結果:

Context: Transition to StopState
StopState: Start to open 
StopState: lift is opened 
Context: Transition to OpenState
OpenState: The door start to close
OpenState: The door closed, will run
Context: Transition to RunState
RunState: lift is running 
Context: Transition to RunState
RunState: Start to stop 
RunState: lift Stoped 
Context: Transition to StopState
StopState: Run Forbidden 
Context: Transition to StopState

案例2:
模擬收音機系統,在AM波段時可以跳轉到FM,FM可以跳轉AM;
AM波段有"1250", “1380”, “1510"三個頻道可以切換
FM波段有"81.3”, “89.1”, "103.9"三個頻道可以切換

代碼:

from abc import ABC, abstractmethod

class RadioContext(object):
    _state = None
    def __init__(self, state):
        self.trans_state(state)

    @property
    def state(self):
        return self._state

    def trans_state(self, state):
        self._state = state

    def scan(self):
        return self._state.scan()

    def switch(self):
        return self.trans_state(self._state.switch())


class State(ABC):

    @abstractmethod
    def switch(self):
        pass

    def scan(self):
        """頻道切換"""
        self.pos = self.pos + 1
        if self.pos == len(self.stations):
            self.pos = 0
        print(f'當前是{self.name}, 正在播放的頻道是{self.stations[self.pos]}')

class AmState(State):
    def __init__(self) -> None:
        self.stations = ["1250", "1380", "1510"]
        self.pos = 0
        self.name = "AM"

    def switch(self):
        print("切換到FM")
        return FmState()

class FmState(State):
    def __init__(self) -> None:
        self.stations = ["81.3", "89.1", "103.9"]
        self.pos = 0
        self.name = "FM"

    def switch(self):
        print("切換到AM")
        return AmState()

if __name__ == "__main__":
    radio = RadioContext(AmState())
    actions = [radio.scan] * 2 + [radio.switch] + [radio.scan] * 2
    actions *= 2

    for action in actions:
        action()

運行結果:

當前是AM, 正在播放的頻道是1380
當前是AM, 正在播放的頻道是1510
切換到FM
當前是FM, 正在播放的頻道是89.1
當前是FM, 正在播放的頻道是103.9
當前是FM, 正在播放的頻道是81.3
當前是FM, 正在播放的頻道是89.1
切換到AM
當前是AM, 正在播放的頻道是1380
當前是AM, 正在播放的頻道是1510

案例3:
一個典型操作系統進程的狀態圖(不是針對特定的系統)。進程一開始由用戶創建好,就進入“已創建/新建”狀態。
這個狀態只能切換到“等待”狀態,這個狀態轉換髮生在調度器將進程加載進內存並添加到“等待/預備執行”的進程隊列之時。
一個“等待”進程有兩個可能的狀態轉換:可被選擇而執行(切換到“運行”狀態),或被更高優先級的進程所替代(切換到“換出並等待”狀態)。
進程的其他典型狀態還包括“終止”(已完成或已終止)、“阻塞”(例如,等待一個I/O操作完成)等。
需要注意,一個狀態機在一個特定時間點只能有一個激活狀態。例如,一個進程不可能同時處於“已創建”狀態和“運行”狀態。

代碼:

from state_machine import State, Event, acts_as_state_machine, after, before, InvalidStateTransition

@acts_as_state_machine #創建狀態機
class Process(object):
    #定義狀態機狀態
    created = State(initial=True) #設置inital=True,指定初始狀態
    waiting = State()
    running = State()
    terminated = State()
    blocked = State()
    swapped_out_waiting = State()
    swapped_out_blocked = State()

    #定義狀態轉換
    """
    在state_machine模塊中,一個狀態轉換就是一個Event。我們使用參數from_states和to_state來定義一個可能的轉換。from_states可以是單個狀態或一組狀態(元組)。
    """
    wait = Event(from_states=(created, running, blocked, swapped_out_waiting), to_state=waiting)
    run = Event(from_states=waiting, to_state=running)
    terminate = Event(from_states=running, to_state=terminated)
    block = Event(from_states=(running, swapped_out_blocked), to_state=blocked)
    swap_wait = Event(from_states=waiting, to_state=swapped_out_waiting)
    swap_block = Event(from_states=blocked, to_state=swapped_out_blocked)

    #設置進程名稱
    def __init__(self, name):
        self.name = name

    """
    state_machine模塊提供@before和@after修飾器,用於在狀態轉換之前或之後執行動作。爲了達到示例的目的,這裏的動作限於輸出進程狀態轉換的信息。
    """

    @after('wait')
    def wait_info(self):
        print('{} entered waiting mode'.format(self.name))

    @after('run')
    def run_info(self):
        print('{} is running'.format(self.name))

    @before('terminate')
    def terminate_info(self):
        print('{} terminated'.format(self.name))

    @after('block')
    def block_info(self):
        print('{} is blocked'.format(self.name))

    @after('swap_wait')
    def swap_wait_info(self):
        print('{} is swapped out and waiting'.format(self.name))

    @after('swap_block')
    def swap_block_info(self):
        print('{} is swapped out and blocked'.format(self.name))

"""
transition()函數接受三個參數:process、event和event_name。
process是一個Process類實例,event是一個Event類(wait、run和terminate等)實例,而event_name是事件的名稱。
在嘗試執行event時,如果發生錯誤,則會輸出事件的名稱。
"""
def transition(process, event, event_name):
    try:
        event()
    except InvalidStateTransition as err:
        print('Error: transition of {} from {} to {} failed'.format(process.name, process.current_state, event_name))

def state_info(process):
    print('state of {}: {}'.format(process.name, process.current_state))

def main():
    #字符串常量,作爲event_name參數值傳遞
    RUNNING = 'running'
    WAITING = 'waiting'
    BLOCKED = 'blocked'
    TERMINATED = 'terminated'

    #創建兩個Process實例,並輸出它們的初始狀態信息
    p1, p2 = Process('process1'), Process('process2')
    [state_info(p) for p in (p1, p2)]

    #狀態轉換
    print()
    transition(p1, p1.wait, WAITING)
    transition(p2, p2.terminate, TERMINATED)
    [state_info(p) for p in (p1, p2)]

    print()
    transition(p1, p1.run, RUNNING)
    transition(p2, p2.wait, WAITING)
    [state_info(p) for p in (p1, p2)]

    print()
    transition(p2, p2.run, RUNNING)
    [state_info(p) for p in (p1, p2)]

    print()
    [transition(p, p.block, BLOCKED)
    for p in (p1, p2)]
    [state_info(p) for p in (p1, p2)]
    print()
    [transition(p, p.terminate, TERMINATED) for p in (p1, p2)]
    [state_info(p) for p in (p1, p2)]

if __name__ == '__main__':
    main()

運行結果:

state of process1: created
state of process2: created

process1 entered waiting mode
Error: transition of process2 from created to terminated failed
state of process1: waiting
state of process2: created

process1 is running
process2 entered waiting mode
state of process1: running
state of process2: waiting

process2 is running
state of process1: running
state of process2: running

process1 is blocked
process2 is blocked
state of process1: blocked
state of process2: blocked

Error: transition of process1 from blocked to terminated failed
Error: transition of process2 from blocked to terminated failed
state of process1: blocked
state of process2: blocked

5 應用案例

狀態模式適用於許多問題。所有可以使用狀態機解決的問題都是不錯的狀態模式應用案例。我們已經見過的一個例子是操作系統/嵌入式系統的進程模型。

編程語言的編譯器實現是另一個好例子。詞法和旬法分析可使用狀態來構建抽象語法樹(請參考網頁[t.cn/RUFNdYt])。

事件驅動系統也是另一個例子。在一個事件驅動系統中,從一個狀態轉換到另一個狀態會觸發一個事件/消息。許多計算機遊戲都使用這一技術。例如,怪獸會在主人公接近時從防禦狀態轉換到攻擊狀態(請參考網頁[t.cn/Rqr13Lr] 和網頁[t.cn/Rqr1BW4])。

這裏引用Thomas Jaeger說過的一旬話:“狀態設計模式解決的是一定上下文中尤限數量狀態的完全封裝,從而實現更好的可維護性和靈活性。”(請參考網頁[t.cn/8sZrLP0]。)

6 軟件例子

使用狀態模式本質上相當於實現一個狀態機來解決特定領域的一個軟件問題。django-fsm程序包是一個第三方程序包,用於 Django 框架中簡化狀態機的實現和使用(請參考網頁[t.cn/Rqr1Tgb])。

Python提供不止一個第三方包/模塊來使用和實現狀態機(請參考網頁[t.cn/Rqr1Qdn])。我們將在14.4節中看到如何使用其中的一個。

另一個值得一提的項目是狀態機編譯器(State Machine Compiler,SMC)。使用SMC,你可以使用一種簡單的領域特定語言在文本文件中描述你的狀態機,SMC會自動生成狀態機的代碼。該項目聲稱這種DSL非常簡單,寫起來就像一對一地翻譯一個狀態圖。我沒試過,但聽起來非常有意思。SMC可以生成多種編程語言的代碼,包括Python(請參考網頁[t.cn/RwDrn4v])

github代碼參考:https://github.com/jtushman/state_machine/blob/master/state_machine

7 與其他模式的關係

  • 橋接模式、 狀態模式和策略模式 (在某種程度上包括適配器模式) 模式的接口非常相似。 實際上, 它們都基於組合模式——即將工作委派給其他對象, 不過也各自解決了不同的問題。 模式並不只是以特定方式組織代碼的配方, 你還可以使用它們來和其他開發者討論模式所解決的問題。

  • 狀態可被視爲策略的擴展。 兩者都基於組合機制: 它們都通過將部分工作委派給 “幫手” 對象來改變其在不同情景下的行爲。 策略使得這些對象相互之間完全獨立, 它們不知道其他對象的存在。 但狀態模式沒有限制具體狀- 態之間的依賴, 且允許它們自行改變在不同情景下的狀態。

參考文獻

https://refactoringguru.cn/design-patterns/state
https://www.jianshu.com/p/a32fdea4f1e0

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章