文章內容參考自《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)類就可以實現(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中。