「每天一個知識點」精講設計模式之狀態模式

點擊上方"java全棧技術"關注,每天學習一個java知識點,喜歡的也可以關注WX號"ITeye"

“人有悲歡離合,月有陰晴圓缺”,包括人在內,很多事物都具有多種狀態,而且在不同狀態下會具有不同的行為,這些狀態在特定條件下還將發生相互轉換。就像水,它可以凝固成冰,也可以受熱蒸發後變成水蒸汽,水可以流動,冰可以雕刻,蒸汽可以擴散。我們可以用UML狀態圖來描述H2O的三種狀態,如圖1所示:

「每天一個知識點」精講設計模式之狀態模式

圖1 H2O的三種狀態(未考慮臨界點)

在軟件系統中,有些對象也像水一樣具有多種狀態,這些狀態在某些情況下能夠相互轉換,而且對象在不同的狀態下也將具有不同的行為。為了更好地對這些具有多種狀態的對象進行設計,我們可以使用一種被稱之為狀態模式的設計模式,本章我們將學習用於描述對象狀態及其轉換的狀態模式。

1. 銀行系統中的賬戶類設計

Sunny軟件公司欲為某銀行開發一套信用卡業務系統,銀行賬戶(Account)是該系統的核心類之一,通過分析,Sunny軟件公司開發人員發現在該系統中,賬戶存在三種狀態,且在不同狀態下賬戶存在不同的行為,具體說明如下:

(1) 如果賬戶中餘額大於等於0,則賬戶的狀態為正常狀態(Normal State),此時用戶既可以向該賬戶存款也可以從該賬戶取款;

(2) 如果賬戶中餘額小於0,並且大於-2000,則賬戶的狀態為透支狀態(Overdraft State),此時用戶既可以向該賬戶存款也可以從該賬戶取款,但需要按天計算利息;

(3) 如果賬戶中餘額等於-2000,那麼賬戶的狀態為受限狀態(Restricted State),此時用戶只能向該賬戶存款,不能再從中取款,同時也將按天計算利息;

(4) 根據餘額的不同,以上三種狀態可發生相互轉換。

Sunny軟件公司開發人員對銀行賬戶類進行分析,繪製瞭如圖2所示UML狀態圖:

「每天一個知識點」精講設計模式之狀態模式

圖2 銀行賬戶狀態圖

在圖2中,NormalState表示正常狀態,OverdraftState表示透支狀態,RestrictedState表示受限狀態,在這三種狀態下賬戶對象擁有不同的行為,方法deposit()用於存款,withdraw()用於取款,computeInterest()用於計算利息,stateCheck()用於在每一次執行存款和取款操作後根據餘額來判斷是否要進行狀態轉換並實現狀態轉換,相同的方法在不同的狀態中可能會有不同的實現。為了實現不同狀態下對象的各種行為以及對象狀態之間的相互轉換,Sunny軟件公司開發人員設計了一個較為龐大的賬戶類Account,其中部分代碼如下所示:

「每天一個知識點」精講設計模式之狀態模式

「每天一個知識點」精講設計模式之狀態模式

分析上述代碼,我們不難發現存在如下幾個問題:

(1) 幾乎每個方法中都包含狀態判斷語句,以判斷在該狀態下是否具有該方法以及在特定狀態下該方法如何實現,導致代碼非常冗長,可維護性較差;

(2) 擁有一個較為複雜的stateCheck()方法,包含大量的if…else if…else…語句用於進行狀態轉換,代碼測試難度較大,且不易於維護;

(3) 系統擴展性較差,如果需要增加一種新的狀態,如凍結狀態(Frozen State,在該狀態下既不允許存款也不允許取款),需要對原有代碼進行大量修改,擴展起來非常麻煩。

為了解決這些問題,我們可以使用狀態模式,在狀態模式中,我們將對象在每一個狀態下的行為和狀態轉移語句封裝在一個個狀態類中,通過這些狀態類來分散冗長的條件轉移語句,讓系統具有更好的靈活性和可擴展性,狀態模式可以在一定程度上解決上述問題

2 狀態模式概述

狀態模式用於解決系統中複雜對象的狀態轉換以及不同狀態下行為的封裝問題。當系統中某個對象存在多個狀態,這些狀態之間可以進行轉換,而且對象在不同狀態下行為不相同時可以使用狀態模式。狀態模式將一個對象的狀態從該對象中分離出來,封裝到專門的狀態類中,使得對象狀態可以靈活變化,對於客戶端而言,無須關心對象狀態的轉換以及對象所處的當前狀態,無論對於何種狀態的對象,客戶端都可以一致處理。

狀態模式定義如下:

狀態模式(State Pattern):允許一個對象在其內部狀態改變時改變它的行為,對象看起來似乎修改了它的類。其別名為狀態對象(Objects for States),狀態模式是一種對象行為型模式。

在狀態模式中引入了抽象狀態類和具體狀態類,它們是狀態模式的核心,其結構如圖3所示:

「每天一個知識點」精講設計模式之狀態模式

圖3 狀態模式結構圖

在狀態模式結構圖中包含如下幾個角色:

● Context(環境類):環境類又稱為上下文類,它是擁有多種狀態的對象。由於環境類的狀態存在多樣性且在不同狀態下對象的行為有所不同,因此將狀態獨立出去形成單獨的狀態類。在環境類中維護一個抽象狀態類State的實例,這個實例定義當前狀態,在具體實現時,它是一個State子類的對象。

● State(抽象狀態類):它用於定義一個接口以封裝與環境類的一個特定狀態相關的行為,在抽象狀態類中聲明瞭各種不同狀態對應的方法,而在其子類中實現類這些方法,由於不同狀態下對象的行為可能不同,因此在不同子類中方法的實現可能存在不同,相同的方法可以寫在抽象狀態類中。

● ConcreteState(具體狀態類):它是抽象狀態類的子類,每一個子類實現一個與環境類的一個狀態相關的行為,每一個具體狀態類對應環境的一個具體狀態,不同的具體狀態類其行為有所不同。

在狀態模式中,我們將對象在不同狀態下的行為封裝到不同的狀態類中,為了讓系統具有更好的靈活性和可擴展性,同時對各狀態下的共有行為進行封裝,我們需要對狀態進行抽象,引入了抽象狀態類角色,其典型代碼如下所示:

「每天一個知識點」精講設計模式之狀態模式

環境類維持一個對抽象狀態類的引用,通過setState()方法可以向環境類注入不同的狀態對象,再在環境類的業務方法中調用狀態對象的方法,典型代碼如下所示:

「每天一個知識點」精講設計模式之狀態模式

環境類實際上是真正擁有狀態的對象,我們只是將環境類中與狀態有關的代碼提取出來封裝到專門的狀態類中。在狀態模式結構圖中,環境類Context與抽象狀態類State之間存在單向關聯關係,在Context中定義了一個State對象。在實際使用時,它們之間可能存在更為複雜的關係,State與Context之間可能也存在依賴或者關聯關係。

在狀態模式的使用過程中,一個對象的狀態之間還可以進行相互轉換,通常有兩種實現狀態轉換的方式:

(1) 統一由環境類來負責狀態之間的轉換,此時,環境類還充當了狀態管理器(State Manager)角色,在環境類的業務方法中通過對某些屬性值的判斷實現狀態轉換,還可以提供一個專門的方法用於實現屬性判斷和狀態轉換,如下代碼片段所示:

「每天一個知識點」精講設計模式之狀態模式

(2) 由具體狀態類來負責狀態之間的轉換,可以在具體狀態類的業務方法中判斷環境類的某些屬性值再根據情況為環境類設置新的狀態對象,實現狀態轉換,同樣,也可以提供一個專門的方法來負責屬性值的判斷和狀態轉換。此時,狀態類與環境類之間就將存在依賴或關聯關係,因為狀態類需要訪問環境類中的屬性值,如下代碼片段所示:

「每天一個知識點」精講設計模式之狀態模式

3 完整解決方案

Sunny軟件公司開發人員使用狀態模式來解決賬戶狀態的轉換問題,客戶端只需要執行簡單的存款和取款操作,系統根據餘額將自動轉換到相應的狀態,其基本結構如圖4所示:

「每天一個知識點」精講設計模式之狀態模式

圖4 銀行賬戶結構圖

在圖4中,Account充當環境類角色,AccountState充當抽象狀態角色,NormalState、OverdraftState和RestrictedState充當具體狀態角色。完整代碼如下所示:

「每天一個知識點」精講設計模式之狀態模式

「每天一個知識點」精講設計模式之狀態模式

「每天一個知識點」精講設計模式之狀態模式

「每天一個知識點」精講設計模式之狀態模式

「每天一個知識點」精講設計模式之狀態模式

「每天一個知識點」精講設計模式之狀態模式

「每天一個知識點」精講設計模式之狀態模式

「每天一個知識點」精講設計模式之狀態模式

「每天一個知識點」精講設計模式之狀態模式

編寫如下客戶端測試代碼:

「每天一個知識點」精講設計模式之狀態模式

編譯並運行程序,輸出結果如下:

段譽開戶,初始金額為0.0

---------------------------------------------

段譽存款1000.0

現在餘額為1000.0

現在帳戶狀態為NormalState

---------------------------------------------

段譽取款2000.0

現在餘額為-1000.0

現在帳戶狀態為OverdraftState

---------------------------------------------

段譽存款3000.0

現在餘額為2000.0

現在帳戶狀態為NormalState

---------------------------------------------

段譽取款4000.0

現在餘額為-2000.0

現在帳戶狀態為RestrictedState

---------------------------------------------

段譽取款1000.0

帳號受限,取款失敗

現在餘額為-2000.0

現在帳戶狀態為RestrictedState

---------------------------------------------

計算利息!

4 共享狀態

在有些情況下,多個環境對象可能需要共享同一個狀態,如果希望在系統中實現多個環境對象共享一個或多個狀態對象,那麼需要將這些狀態對象定義為環境類的靜態成員對象

下面通過一個簡單實例來說明如何實現共享狀態:

如果某系統要求兩個開關對象要麼都處於開的狀態,要麼都處於關的狀態,在使用時它們的狀態必須保持一致,開關可以由開轉換到關,也可以由關轉換到開。

可以使用狀態模式來實現開關的設計,其結構如圖5所示:

「每天一個知識點」精講設計模式之狀態模式

圖5 開關及其狀態設計結構圖

開關類Switch代碼如下所示:

「每天一個知識點」精講設計模式之狀態模式

「每天一個知識點」精講設計模式之狀態模式

抽象狀態類如下代碼所示:

「每天一個知識點」精講設計模式之狀態模式

兩個具體狀態類如下代碼所示:

「每天一個知識點」精講設計模式之狀態模式

編寫如下客戶端代碼進行測試:

「每天一個知識點」精講設計模式之狀態模式

輸出結果如下:

開關1已經打開!

開關2已經打開!

開關1關閉!

開關2已經關閉!

開關2打開!

開關1已經打開!

從輸出結果可以得知兩個開關共享相同的狀態,如果第一個開關關閉,則第二個開關也將關閉,再次關閉時將輸出“已經關閉”;打開時也將得到類似結果。

5 使用環境類實現狀態轉換

在狀態模式中實現狀態轉換時,具體狀態類可通過調用環境類Context的setState()方法進行狀態的轉換操作,也可以統一由環境類Context來實現狀態的轉換。此時,增加新的具體狀態類可能需要修改其他具體狀態類或者環境類的源代碼,否則系統無法轉換到新增狀態。但是對於客戶端來說,無須關心狀態類,可以為環境類設置默認的狀態類,而將狀態的轉換工作交給具體狀態類或環境類來完成,具體的轉換細節對於客戶端而言是透明的。

在上面的“銀行賬戶狀態轉換”實例中,我們通過具體狀態類來實現狀態的轉換,在每一個具體狀態類中都包含一個stateCheck()方法,在該方法內部實現狀態的轉換,除此之外,我們還可以通過環境類來實現狀態轉換,

環境類作為一個狀態管理器,統一實現各種狀態之間的轉換操作

下面通過一個包含循環狀態的簡單實例來說明如何使用環境類實現狀態轉換:

Sunny軟件公司某開發人員欲開發一個屏幕放大鏡工具,其具體功能描述如下:

用戶單擊“放大鏡”按鈕之後屏幕將放大一倍,再點擊一次“放大鏡”按鈕屏幕再放大一倍,第三次點擊該按鈕後屏幕將還原到默認大小。

可以考慮使用狀態模式來設計該屏幕放大鏡工具,我們定義三個屏幕狀態類NormalState、LargerState和LargestState來對應屏幕的三種狀態,分別是正常狀態、二倍放大狀態和四倍放大狀態,屏幕類Screen充當環境類,其結構如圖6所示:

「每天一個知識點」精講設計模式之狀態模式

圖6 屏幕放大鏡工具結構圖

本實例核心代碼如下所示:

「每天一個知識點」精講設計模式之狀態模式

「每天一個知識點」精講設計模式之狀態模式

「每天一個知識點」精講設計模式之狀態模式

「每天一個知識點」精講設計模式之狀態模式

在上述代碼中,所有的狀態轉換操作都由環境類Screen來實現,此時,環境類充當了狀態管理器角色。如果需要增加新的狀態,例如“八倍狀態類”,需要修改環境類,這在一定程度上違背了“開閉原則”,但對其他狀態類沒有任何影響。

編寫如下客戶端代碼進行測試:

「每天一個知識點」精講設計模式之狀態模式

輸出結果如下:

正常大小!

二倍大小!

四倍大小!

正常大小!

6 狀態模式總結

狀態模式將一個對象在不同狀態下的不同行為封裝在一個個狀態類中,通過設置不同的狀態對象可以讓環境對象擁有不同的行為,而狀態轉換的細節對於客戶端而言是透明的,方便了客戶端的使用。在實際開發中,狀態模式具有較高的使用頻率,在工作流和遊戲開發中狀態模式都得到了廣泛的應用,例如公文狀態的轉換、遊戲中角色的升級等。

1. 主要優點

狀態模式的主要優點如下:

(1) 封裝了狀態的轉換規則,在狀態模式中可以將狀態的轉換代碼封裝在環境類或者具體狀態類中,可以對狀態轉換代碼進行集中管理,而不是分散在一個個業務方法中。

(2) 將所有與某個狀態有關的行為放到一個類中,只需要注入一個不同的狀態對象即可使環境對象擁有不同的行為。

(3) 允許狀態轉換邏輯與狀態對象合成一體,而不是提供一個巨大的條件語句塊,狀態模式可以讓我們避免使用龐大的條件語句來將業務方法和狀態轉換代碼交織在一起。

(4) 可以讓多個環境對象共享一個狀態對象,從而減少系統中對象的個數。

2. 主要缺點

狀態模式的主要缺點如下:

(1) 狀態模式的使用必然會增加系統中類和對象的個數,導致系統運行開銷增大

(2) 狀態模式的結構與實現都較為複雜,如果使用不當將導致程序結構和代碼的混亂,增加系統設計的難度

(3) 狀態模式對“開閉原則”的支持並不太好,增加新的狀態類需要修改那些負責狀態轉換的源代碼,否則無法轉換到新增狀態;而且修改某個狀態類的行為也需修改對應類的源代碼。

3. 適用場景

在以下情況下可以考慮使用狀態模式:

(1) 對象的行為依賴於它的狀態(如某些屬性值),狀態的改變將導致行為的變化。

(2) 在代碼中包含大量與對象狀態有關的條件語句,這些條件語句的出現,會導致代碼的可維護性和靈活性變差,不能方便地增加和刪除狀態,並且導致客戶類與類庫之間的耦合增強。

原文:http://blog.csdn.net/lovelion


分享到:


相關文章: