python設計模式-狀態模式

問題:有一個糖果公司需要設計一個糖果售賣機,控制流程如下圖,需要怎麼實現?

1b0d0134acf9ab9b2240066f847412f1.png

這是一個狀態圖,每個圓圈都是一種狀態。很明顯,有有25分錢沒有25分錢售出糖果糖果售罄四個狀態,同時也對應四個動作投入25分錢退回25分錢轉動曲柄發放糖果

那如何從狀態圖得到真正的代碼呢?

簡單代碼實現如下:

#! -*- coding: utf-8 -*-

class GumballMachine:

    # 找出所有狀態,並創建實例變量來持有當前狀態,然後定義狀態的值
    STATE_SOLD_OUT = 0
    STATE_NO_QUARTER = 1
    STATE_HAS_QUARTER = 2
    STATE_SOLD = 3

    state = STATE_SOLD_OUT

    def __init__(self, count=0):
        self.count = count
        if count > 0:
            self.state = self.STATE_NO_QUARTER

    def __str__(self):
        return "Gumball machine current state: %s" % self.state

    def insert_quarter(self):
        # 投入25分錢
        if self.state == self.STATE_HAS_QUARTER: # 如果已經投過
            print("You can't insert another quarter")
        elif self.state == self.STATE_NO_QUARTER: # 如果沒有投過
            self.state = self.STATE_HAS_QUARTER
            print("You inserted a quarter")
        elif self.state == self.STATE_SOLD_OUT: # 如果已經售罄
            print("You can't insert a quarter, the machine is sold out")
        elif self.state == self.STATE_SOLD: # 如果剛剛買了糖果
            print("Please wait, we're already giving you a gumball")

    def eject_quarter(self):
        # 退回25分
        if self.state == self.STATE_HAS_QUARTER:
            print("Quarter returned")
            self.state = self.STATE_NO_QUARTER
        elif self.state == self.STATE_NO_QUARTER:
            print("You haven't inserted a quarter")
        elif self.state == self.STATE_SOLD:
            print("Sorry, you alread turned the crank")
        elif self.state == self.SOLD_OUT:
            print("You can't eject, you haven't inserted")

    def turn_crank(self):
        # 轉動曲柄
        if self.state == self.STATE_SOLD:
            print("Turning twice doesn't get you another gumball")
        elif self.state == self.STATE_NO_QUARTER:
            print("You turned but there's no quarter")
        elif self.state == self.STATE_SOLD_OUT:
            print("You turned, but there are no gumballs")
        elif self.state == self.STATE_HAS_QUARTER:
            print("You turned...")
            self.state = self.STATE_SOLD
            self.dispense()
    
    def dispense(self):
        # 發放糖果
        if self.state == self.STATE_SOLD:
            print("A gumball comes rolling out the slot")
            self.count -= 1
            if self.count == 0:
                self.state = self.STATE_SOLD_OUT
            else:
                self.state = self.STATE_NO_QUARTER
        elif self.state == self.STATE_NO_QUARTER:
            print("You need to pay first")
        elif self.state == self.STATE_SOLD_OUT:
            print("No gumball dispensed")
        elif self.state == self.STATE_HAS_QUARTER:
            print("No gumball dispensed")


if __name__ == "__main__":
    # 以下是代碼測試
    gumball_machine = GumballMachine(5) # 裝入5 個糖果
    print(gumball_machine)

    gumball_machine.insert_quarter() # 投入25分錢
    gumball_machine.turn_crank() # 轉動曲柄
    print(gumball_machine)

    gumball_machine.insert_quarter() #投入25分錢
    gumball_machine.eject_quarter()  # 退錢
    gumball_machine.turn_crank()     # 轉動曲柄

    print(gumball_machine)
    
    gumball_machine.insert_quarter() # 投入25分錢
    gumball_machine.turn_crank() # 轉動曲柄 
    gumball_machine.insert_quarter() # 投入25分錢 
    gumball_machine.turn_crank()  # 轉動曲柄
    gumball_machine.eject_quarter() # 退錢

    print(gumball_machine)

這段代碼有幾個問題:

  1. 沒有遵守開放-關閉原則
  2. 更像是面向過程的設計
  3. 狀態轉化被埋藏在條件語句中
  4. 未來加入新的需求,需要改動的較多,不易維護,可能會出bug

如何改進呢?

考慮封裝變化,把每個狀態的行爲都放在各自的類中,每個狀態只要實現自己的動作,用加入新類的方式來實現新狀態的加入。
  1. 定義State 父類,在這個類中,糖果機的每個動作都有一個應對的方法
  2. 爲機器中的每個狀態實現狀態類,這些類將負責在對應的狀態下進行機器的行爲
  3. 擺脫舊的條件代碼,將動作委託到狀態類

新的實現代碼如下:

#! -*- coding: utf-8 -*-

class State:
    # 定義state基類
    def insert_quarter(self):
        pass

    def eject_quarter(self):
        pass

    def turn_crank(self):
        pass

    def dispense(self):
        pass


class SoldOutState(State):
    # 繼承State 類
    def __init__(self, gumball_machine):
        self.gumball_machine = gumball_machine

    def __str__(self):
        return "sold_out"

    def insert_quarter(self):
        print("You can't insert a quarter, the machine is sold out")

    def eject_quarter(self):
        print("You can't eject, you haven't inserted a quarter yet")

    def turn_crank(self):
        print("You turned, but ther are no gumballs")

    def dispense(self):
        print("No gumball dispensed")


class SoldState(State):
    # 繼承State 類
    def __init__(self, gumball_machine):
        self.gumball_machine = gumball_machine

    def __str__(self):
        return "sold"

    def insert_quarter(self):
        print("Please wait, we're already giving you a gumball")

    def eject_quarter(self):
        print("Sorry, you already turned the crank")

    def turn_crank(self):
        print("Turning twice doesn't get you another gumball")

    def dispense(self):
        self.gumball_machine.release_ball()
        if gumball_machine.count > 0:
            self.gumball_machine.state = self.gumball_machine.no_quarter_state
        else:
            print("Oops, out of gumballs!")
            self.gumball_machine.state = self.gumball_machine.soldout_state


class NoQuarterState(State):
    # 繼承State 類
    def __init__(self, gumball_machine):
        self.gumball_machine = gumball_machine

    def __str__(self):
        return "no_quarter"

    def insert_quarter(self):
        # 投幣 並且改變狀態
        print("You inserted a quarter")
        self.gumball_machine.state = self.gumball_machine.has_quarter_state

    def eject_quarter(self):
        print("You haven't insert a quarter")

    def turn_crank(self):
        print("You turned, but there's no quarter")

    def dispense(self):
        print("You need to pay first")


class HasQuarterState(State):
    # 繼承State 類
    def __init__(self, gumball_machine):
        self.gumball_machine = gumball_machine

    def __str__(self):
        return "has_quarter"

    def insert_quarter(self):
        print("You can't insert another quarter")

    def eject_quarter(self):
        print("Quarter returned")
        self.gumball_machine.state = self.gumball_machine.no_quarter_state

    def turn_crank(self):
        print("You turned...")
        self.gumball_machine.state = self.gumball_machine.sold_state

    def dispense(self):
        print("No gumball dispensed")


class GumballMachine:

    def __init__(self, count=0):
        self.count = count
        # 找出所有狀態,並創建實例變量來持有當前狀態,然後定義狀態的值
        self.soldout_state = SoldOutState(self)
        self.no_quarter_state = NoQuarterState(self)
        self.has_quarter_state = HasQuarterState(self)
        self.sold_state = SoldState(self)
        if count > 0:
            self.state = self.no_quarter_state
        else:
            self.state = self.soldout_state

    def __str__(self):
        return ">>> Gumball machine current state: %s" % self.state

    def insert_quarter(self):
        # 投入25分錢
        self.state.insert_quarter()

    def eject_quarter(self):
        # 退回25分
        self.state.eject_quarter()
        # print("state", self.state, type(self.state))

    def turn_crank(self):
        # 轉動曲柄
        # print("state", self.state, type(self.state))
        self.state.turn_crank()
    
    def release_ball(self):
        # 發放糖果
        print("A gumball comes rolling out the slot...")
        if self.count > 0:
            self.count -= 1
        
        
if __name__ == "__main__":
    # 以下是代碼測試
    gumball_machine = GumballMachine(5) # 裝入5 個糖果
    print(gumball_machine)

    gumball_machine.insert_quarter() # 投入25分錢
    gumball_machine.turn_crank() # 轉動曲柄
    print(gumball_machine)

    gumball_machine.insert_quarter() #投入25分錢
    gumball_machine.eject_quarter()  # 退錢
    gumball_machine.turn_crank()     # 轉動曲柄

    print(gumball_machine)
    
    gumball_machine.insert_quarter() # 投入25分錢
    gumball_machine.turn_crank() # 轉動曲柄 
    gumball_machine.insert_quarter() # 投入25分錢 
    gumball_machine.turn_crank()  # 轉動曲柄
    gumball_machine.eject_quarter() # 退錢

    print(gumball_machine)

重構後的代碼相對於之前的代碼做了哪些事情呢?

  1. 將每個狀態的行爲局部話到自己的類中
  2. 刪除if 語句
  3. 狀態類對修改關閉,對糖果季類對擴展開放

下圖是剛初始狀態圖示:

上面重構部分代碼使用的就是狀態模式:

定義

狀態模式: 狀態模式允許對象在內部狀態改變時改變它的行爲,對象看起來好像修改了它的類。

狀態模式的類圖如下:

狀態模式是將多個行爲封裝在狀態對象中, context 的行爲隨時可委託到其中一個狀態中。當前狀態在不同的狀態對象中改變,以反映出context 內部的狀態,context 的行爲也會隨之改變。

擴展

如果,現在要在這四個狀態的基礎上再加一個狀態(購買糖果後,有10%的概率再得一個),該如何實現呢?
# 添加WinnerState 類,只有dispense 方法不同,可以從SoldState 類繼承
class WinnerState(SoldState):
    
    def __str__(self):
        return "winner"

    def dispense(self):
        print("You're a WINNER! You get two gumballs for your quarter")
        self.gumball_machine.release_ball()
        if gumball_machine.count == 0:
            self.gumball_machine.state = self.gumball_machine.soldout_state
        else:
            self.gumball_machine.release_ball()
            if gumball_machine.count > 0:
                self.gumball_machine.state = self.gumball_machine.no_quarter_state
            else:
                print("Oops, out of gumballs!")
                self.gumball_machine.state = self.gumball_machine.soldout_state

# 修改turn_crank 方法
class HasQuarterState(State):
    ...
    def turn_crank(self):
        print("You turned...")
        winner = random.randint(0, 9)
        if winner == 4 and self.gumball_machine.count > 1: # 如果庫存大於 1 並且隨機數等於4(可以是0到9任意值)
            self.gumball_machine.state = self.gumball_machine.winner_state
        else:
            self.gumball_machine.state = self.gumball_machine.sold_state


# 在 GumballMachine 中初始化
class GumballMachine:

    def __init__(self, count=0):
        self.count = count
        # 找出所有狀態,並創建實例變量來持有當前狀態,然後定義狀態的值
        ...
        self.winner_state = WinnerState(self)
        ...

總結

  1. 狀態模式允許一個對象給予內部狀態而擁有不同的行爲
  2. 狀態模式用類代表狀態
  3. Context 會將行爲委託給當前狀態對象
  4. 通過將每狀態封裝進一個類,把改變局部化
  5. 狀態裝歡可以由State 類或Context 類控制
  6. 使用狀態模式會增加類的數目
  7. 狀態類可以被多個Context 實例共享

本文例子來自《Head First 設計模式》。

最後,感謝女朋友支持和包容,比❤️

也可以在公號輸入以下關鍵字獲取歷史文章:公號&小程序 | 設計模式 | 併發&協程

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