設計模式之state模式


文章內容參考自《Head First設計模式》的狀態(tài)模式這一節(jié)。

一、state模式

  • 定義: 狀態(tài)模式允許對象在內部狀態(tài)改變時改變他的行為,對象看起來好像修改了他的類。
  • 主要解決:對象的行為依賴于它的狀態(tài)(屬性),并且可以根據(jù)它的狀態(tài)改變而改變它的相關行為。
  • 何時使用:代碼中包含大量與對象狀態(tài)有關的條件語句。

二、模式類圖

模式圖

三、案例

某糖果公司想要發(fā)明一個萬能糖果機??刂破鞯墓ぷ髁鞒绦枰缦聢D所示:

控制器工作流程

這張圖是一個狀態(tài)圖,每一個圓圈代表一個狀態(tài),沒有25分錢是初始狀態(tài),等著你把錢放進來。每一個狀態(tài)都代表機器不同的配置以某種方式行動,需要某些動作將目前的狀態(tài)轉換到另一個狀態(tài)。

四、用模式實現(xiàn)

從上圖可以看出,如何只是普通的實現(xiàn)的話,代碼中會包含大量的if else語句(因為每個動作都要進行狀態(tài)的判斷)。即使實現(xiàn)出來,那么如果再加入新的狀態(tài),代碼修改起來會很困難。幸運的是我們有狀態(tài)模式來幫助我們解決這一難題。

  • 首先我們要定義一個抽象類State,其擁有的方法直接映射到糖果機可能發(fā)生的動作。
    State.py
# coding=utf-8
from abc import ABCMeta, abstractmethod


class State(object):
    metaclass = ABCMeta

    def __init__(self, gumballMachine):
        """糖果機的引用,將他記錄在實例變量中"""
        self.gumballMachine = gumballMachine

    @abstractmethod
    def insertQuarter(self):
        """投入25分錢的動作"""
        pass

    @abstractmethod
    def ejectQuarter(self):
        """退回25分錢的動作"""
        pass

    @abstractmethod
    def turnCrank(self):
        """轉動曲柄"""
        pass

    @abstractmethod
    def dispense(self):
        """發(fā)放糖果"""
        pass

  • 對于有錢、沒錢、售出糖果、告罄四個狀態(tài)繼承State狀態(tài)
    NoQuarterState.py
# coding=utf-8
from State import State


class NoQuarterState(State):
    """state: 沒有25分錢"""
    def __init__(self, gumballMachine):
        State.__init__(self, gumballMachine)

    def insertQuarter(self):
        print "You inserted a quarter"
        # 投入錢后,狀態(tài)切換到有錢狀態(tài)
        self.gumballMachine.setState(self.gumballMachine.getHasQuarterState())

    def ejectQuarter(self):
       print "You haven't inserted a quarter"

    def turnCrank(self):
       print "You turned, but there is no quarter"

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

HasQuarterState.py

# coding=utf-8
from State import State


class HasQuarterState(State):
    """state: 有25分錢"""
    def __init__(self, gumballMachine):
        State.__init__(self, gumballMachine)

    # 這是此狀態(tài)一個不恰當?shù)膭幼?    def insertQuarter(self):
        print "You can't insert another quarter"

    def ejectQuarter(self):
        print "Quarter returned"
        # 退出錢后,將狀態(tài)切換到NoQuarterState狀態(tài)
        self.gumballMachine.setState(self.gumballMachine.getNoQuarterState())

    def turnCrank(self):
        print "You turned...."
        # 轉動后,切換狀態(tài)為售出糖果狀態(tài)。
        self.gumballMachine.setState(self.gumballMachine.getSoldState())

    # 另一個不恰當?shù)膭幼?    def dispense(self):
        print "No gumball dispense"

SoldState.py

# coding=utf-8
from State import State


class SoldState(State):
    """state:售出糖果"""
    def __init__(self, gumballMachine):
        State.__init__(self, gumballMachine)

    # 對于此狀態(tài)來說,以下三種動作都是不恰當?shù)摹?    def insertQuarter(self):
        print "Please wait, we're already giving you a gumball!"

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

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

    # 根據(jù)工作流程圖,此動作才是此狀態(tài)真正的動作。
    def dispense(self):
        # 現(xiàn)在是SoldState狀態(tài),顧客已經付過錢,發(fā)放糖果。
        self.gumballMachine.releaseBall()

        # 檢查糖果機的剩余數(shù)目,切換到不同的狀態(tài)
        if self.gumballMachine.getCount() > 0:
            self.gumballMachine.setState(self.gumballMachine.getNoQuarterState())
        else:
            print "Oops, out of gumballs"
            self.gumballMachine.setState(self.gumballMachine.getSoldOutState())

SoldOutState.py

# coding=utf-8
from State import State


class SoldOutState(State):
    """state: 糖果告罄"""
    def __init__(self, gumballMachine):
        State.__init__(self, gumballMachine)

    def insertQuarter(self):
        print "the machine is sold out"

    def ejectQuarter(self):
        print "the machine is sold out"

    def turnCrank(self):
        print "the machine is sold out"

    def dispense(self):
        print "the machine is sold out"
  • 定義客戶感興趣的接口,GumballMachine類
    GumballMachine.py
# coding=utf-8
from HasQuarterState import HasQuarterState
from NoQuarterState import NoQuarterState
from SoldState import SoldState
from SoldOutState import SoldOutState


class GumballMachine(object):
    """糖果機"""
    def __init__(self, numberGumballs):  # 初始糖果數(shù)目
        # 每一種狀態(tài)都創(chuàng)造一個狀態(tài)實例
        self.soldOutState = SoldOutState(self)
        self.noQuarterState = NoQuarterState(self)
        self.hasQuarterState = HasQuarterState(self)
        self.soldState = SoldState(self)

        self.count = numberGumballs
        if self.count > 0:   # 如果糖果數(shù)目 > 0,就把狀態(tài)設為NoQuarterState
            self.state = self.noQuarterState

        else:
            # 初始狀態(tài)為SoldOutState
            self.state = self.soldOutState

    def insertQuarter(self):
        self.state.insertQuarter()

    def ejectQuarter(self):
        self.state.ejectQuarter()

    def turnCrank(self):
        self.state.turnCrank()
        # 他是一個內部動作,用戶不允許直接要求機器發(fā)糖果,所以不用在此類中去實現(xiàn)它
        self.state.dispense()

    def setState(self, state):
        self.state = state

    def getHasQuarterState(self):
        """返回HasQuarterState的狀態(tài),供具體狀態(tài)類調用進行狀態(tài)轉換"""
        return self.hasQuarterState

    def getNoQuarterState(self):
        return self.noQuarterState

    def getSoldState(self):
        return self.soldState

    def getSoldOutState(self):
        return self.soldOutState

    def releaseBall(self):
        """這個機器提供了一個releaseBall()的輔助函數(shù)來釋放糖果,并將count減1"""
        print "A gumball comes rolling out the slot"
        if self.count != 0:
            self.count -= 1

    def getCount(self):
        return self.count
  • 測試必不可少
    main.py
# coding=utf-8

from GumballMachine import GumballMachine


if __name__ == '__main__':
    # 初始化糖果機糖果數(shù)目為5
    gumball_machine = GumballMachine(5)

    # 打印出機器的初始狀態(tài)
    print gumball_machine.state

    # 投入一枚25分錢硬幣,轉動曲柄,拿到糖果
    gumball_machine.insertQuarter()
    gumball_machine.turnCrank()

    # 再次打印出機器的狀態(tài)
    print gumball_machine.state

    # 投入一枚25分錢,要求機器退錢,轉動曲柄
    gumball_machine.insertQuarter()
    gumball_machine.ejectQuarter()
    gumball_machine.turnCrank()

    # 再次打印出機器的狀態(tài)
    print gumball_machine.state

    gumball_machine.insertQuarter()
    gumball_machine.turnCrank()
    gumball_machine.insertQuarter()
    gumball_machine.turnCrank()
    # 取出糖果后,要求機器退錢
    gumball_machine.ejectQuarter()

    # 再次打印出機器的狀態(tài)
    print gumball_machine.state

    # 壓力測試
    gumball_machine.insertQuarter()
    gumball_machine.insertQuarter()
    gumball_machine.turnCrank()
    gumball_machine.insertQuarter()
    gumball_machine.turnCrank()
    gumball_machine.ejectQuarter()

    # 再次打印出機器的狀態(tài)
    print gumball_machine.state

可以看出我們的代碼實現(xiàn)了以下幾點:

  • 將每個狀態(tài)的行為局部化到他自己的類中。
  • 將容易產生問題的if語句避免掉,以便日后的維護。
  • 讓每個狀態(tài)“對修改關閉”、讓糖果機“對擴展開發(fā)”,因為可以加入新的狀態(tài)類。
  • 容易理解和閱讀。

五、需求變更

新的需求: 糖果公司認為將“購買糖果”變成是一個游戲,可以大大增加他們的銷售量。當曲柄轉動時,有10%的幾率掉下來的是兩顆糖果(多送你一顆)
新的需求出現(xiàn),幸虧當時我們使用了狀態(tài)模式,使這一問題變得很容易。我們先來看一下新的狀態(tài)圖。

新的狀態(tài)圖

可以看出新的狀態(tài)圖增加了一個新的狀態(tài):贏家,那我們只要新增加一個贏家狀態(tài)類就可以實現(xiàn)這一需求。

  • 新增一個WinnerState類,繼承State類。

WinnerState.py

# coding=utf-8
from State import State


class WinnerState(State):
    """state:贏家"""
    def __init__(self, gumballMachine):
        State.__init__(self, gumballMachine)

        # 對于此狀態(tài)來說,以下三種動作都是不恰當?shù)摹?
    def insertQuarter(self):
        print "Please wait, we're already giving you a gumball!"

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

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

    def dispense(self):
        print "You win, you get two gumball"
        # 發(fā)放糖果
        self.gumballMachine.releaseBall()

        if self.gumballMachine.getCount() == 0:
            self.gumballMachine.setState(self.gumballMachine.getSoldOutState())
        else:
            # 發(fā)放第二顆糖
            self.gumballMachine.releaseBall()

            if self.gumballMachine.getCount() > 0:
                self.gumballMachine.setState(self.gumballMachine.getNoQuarterState())
            else:
                print "Oops, out of gumballs"
                self.gumballMachine.setState(self.gumballMachine.getSoldOutState())

相對于SoldState只是dispense做了一點更改。但是我們還需要實現(xiàn)隨機數(shù),還要增加一個進入Winnerstate狀態(tài)的轉換。這兩件事都要加入HasQuarterState,因為顧客會從這個狀態(tài)轉動曲柄。改變后的HasQuarterState為:

# coding=utf-8
from State import State
import random


class HasQuarterState(State):
    """state: 有25分錢"""
    def __init__(self, gumballMachine):
        State.__init__(self, gumballMachine)

    # 這是此狀態(tài)一個不恰當?shù)膭幼?    def insertQuarter(self):
        print "You can't insert another quarter"

    def ejectQuarter(self):
        print "Quarter returned"
        # 退出錢后,將狀態(tài)切換到NoQuarterState狀態(tài)
        self.gumballMachine.setState(self.gumballMachine.getNoQuarterState())

    def turnCrank(self):
        print "You turned...."
        winner = random.randint(0, 9)
        if (winner == 0) and (self.gumballMachine.getCount() > 1):
            # 贏了之后,轉換狀態(tài)為贏家
            self.gumballMachine.setState(self.gumballMachine.getWinnerState())
        else:
            # 轉動后,切換狀態(tài)為售出糖果狀態(tài)。
            self.gumballMachine.setState(self.gumballMachine.getSoldState())

    # 另一個不恰當?shù)膭幼?    def dispense(self):
        print "No gumball dispense"

當然GumballMachine中也要進行相應的改變,這里就不再贅述。

總結:

當遇到對象的行為依賴于它的狀態(tài)(屬性),并且可以根據(jù)它的狀態(tài)改變而改變它的相關行為時,我們選擇狀態(tài)模式。
首先我們找到內部的所有狀態(tài)行為,把行為封裝起來形成接口(State類),然后,每個具體的狀態(tài)類(ConcreteState)去實現(xiàn)這個接口。當外部系統(tǒng)通過Context訪問這些行為時,Context將行為委托到當前狀態(tài)去處理。
另注:
狀態(tài)裝換可以由Context控制,也可以由State控制。一般來說,當狀態(tài)是固定的時候,就適合放在Context中,然而,當轉換是更動態(tài)的時候,通常會放在State中。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容