說到狀態模式,如果你看過之前發布的重構系列的文章中的《代碼重構(六):代碼重構完整案例》這篇博客的話,那么你應該對“狀態模式”並不陌生,因為我們之前使用到了狀態模式進行重構。上一篇博客我們講的主題是“組合模式”,我們使用組合模式創建了一個樹形結構,並給出了遍歷方式。今天我們來認識一下另一種模式,那就是“狀態模式”,今天就從銀行的ATM自動取款機中的取款流程來學習一下狀態模式。
還是老規矩,開門見山。下方是狀態模式的定義:
狀態模式:允許對象在內部狀態改變時改變它的行為,對象看起來好像修夠了它的類。
其實狀態模式與策略模式的共同點非常之多,可以說狀態模式是策略模式的升級版本。關於策略模式的內容,請參加之前關於策略模式的博客《設計模式(一):“穿越火線”中的“策略模式”(Strategy Pattern)》。今天我們就從取款機中的各種狀態,以及各種狀態間的切換來學習一下狀態模式。我們先給出沒有狀態模式的實現方案,然后根據其產生的問題類使用狀態模式進行重構。廢話少說,進入今天的主題。
一、無“狀態模式”的ATM
該部分給出了無“狀態模式”的ATM機的具體實現,該部分先對各種狀態已經各種狀態間的轉換進行分析,然后給出個個狀態之間的關系,有點狀態圖的意思。然后在根據此狀態圖來實現我們的代碼,當然雖然是根據狀態圖實現的代碼,在該部分我們沒有使用狀態模式。所有的狀態轉換我們都在一個ATM的類中進行的。該部分就給出了具體實現。
1.ATM機狀態分析
首先我們先對ATM機的各種狀態,下方就是我們所畫出的類“狀態圖”。每個方框就是一種狀態,而我們的ATM機大致分為無卡,有卡,解密,取款,取出金額,余額不足這六種狀態,也就是下方“狀態圖”中方框中的內容。然后就是動作了,每種狀態間的轉化我們需要動作才可以完成。在我們的ATM機中大致分為插卡,退卡,輸入密碼,輸入金額,確認取款這幾種動作。狀態間轉換時所需的動作關系如下所示。因為該部分實現的代碼位於一個類中,再扯就不做過多的贅述了。
2. 上述狀態關系的具體實現
在代碼實現時,首先我們使用枚舉來列舉出所有的狀態,此處我們命名為ATMState。下方代碼段就是我們ATM機所有的狀態,如下所示:
給出狀態枚舉后,接着我們要實現ATM機的類,下方就是我們ATM機的類。state成員變量就記錄了當前ATM機所處的狀態,默認是無卡狀態。money成員變量記錄了當前取款機中銀行卡的余額,余額默認是0。inputMoney存儲了用戶想提取金額,默認值也是0。insertBankCard()方法則表示插入銀行卡的動作,在執行該動作時,根據ATM機當前所處的狀態來決定要做哪些事情。比如當前已經處於有卡的狀態(HasBankCardState),則會提示“目前已有銀行卡,可以輸入密碼進行取款”,如處於無卡狀態(NoBankCardState),則可以插入銀行卡,並將狀態改為有卡狀態(HasBankCardState),具體請看下方該方法的實現。backBankCard()方法則代表着退卡的動作,在該方法的實現方式與insertBankCard()方法類似,也是根據不同的ATM狀態,執行不同的事情,具體實現如下所示。inputPassword()則表示輸入密碼的動作,inputMoney(money)則代表着輸入取款金額的動作。tapOkButton()方法則代表着點擊取款按鈕的動作。
這些動作的實現方式都差不多,都是根據當前ATM機的狀態來執行一些東西。如果所做的事情會改變ATM機的狀態(比如插入卡),那么在執行完動作后就立即改變ATM機的狀態。下方給出了兩個方法的實現,其他的方法請參考Github分享的完整實例,分享地址見博文結尾部分。
3、測試用例
上面我們給出了ATM機的實現,接下來就是到了測試的時候了,也就是ATM機我們造完了,接下來該使用了。下方就是我們的測試用例,該用例稍稍的有些復雜。首先我們先創建了一個ATM機的對象,然后在無卡狀態下插入銀行卡,緊接着在有卡狀態下有插入一張銀行卡(這個肯定會插入失敗的)。然后在有卡狀態下輸入密碼、輸入正確的金額取款。取款成功后再次輸入密碼和大筆金額(肯定提示余額不足),然后在余額不足的狀態下進行退卡。測試用例具體如下:
上面測試用例運行結果如下。從下方的輸出結果中我們可以看出,無卡狀態下插入卡會從無卡狀態變為有卡狀態。然后在有卡狀態下再次插入銀行卡會提示“目前已有銀行卡,可以輸入密碼進行取款”。有卡狀態下我們可以進行密碼輸入,金額輸入,進行取款。取款成功可以再次輸入密碼進行二次取款,取款時如果余額不足,那么會成為余額不足狀態。在余額不足狀態時可以進行再次取款或者退出銀行卡。說這么多,都是對上面狀態圖的反應。
二、使用“狀態模式”重構
上述代碼雖然能運行,但是問題是非常多的。且不說素有的東西都堆在ATM()這個類中,如果我們添加了一種狀態,那么上面所提到的五個方法都得改變,這顯然是不行的,在此是五個方法,加入是10個,二十個呢,就沒法搞了。所以我們要換一種思路來解決這個問題。那么就是使用“狀態模式”。經過我們的分析,狀態有可能會改變,所以我們要講變化的放在一塊不變的放在一塊。可上面那種設計方式不好將變化的部分進行提取,因為上面代碼是動作中含有各種狀態。
我們換一種思路,就是將狀態含有動作。也就是說一種狀態下有各種操作,而不是上面的一種動作中有各種狀態。這個思路如果能轉變過來,那么我們的狀態模式就好理解多了。如果狀態下含有各種動作的話,當新增一種狀態時只需要將該狀態包括這些動作即可,而不影響其他的狀態。而最初的實現方式新增一種狀態則需要修改每個動作的內容。接下來我們就是要實現“狀態包含不同的動作”,在狀態執行動作時,會根據該狀態下的該動作來對ATM機的當前狀態進行修改,也就是引入“狀態模式”。具體實現方式如下:
1.“類圖”的設計
因為引入“狀態模式”后,我們的ATM機稍微有些復雜,所以在此我們給出了類圖的設計。下方就是我們將要使用代碼進行實現的類圖。要想實現“狀態包含個個動作”的最好的方式就是為每個狀態聲明一個類,然后在類中實現該狀態下的不同的操作。因為我們要依賴接口編程,而不依賴實現編程。所以我們創建了兩個接口,一個是狀態接口StateType,另一個則是ATM機接口ATMType。在StateType中聲明了狀態要包含的動作,因為這些動作也是ATM機的動作,所以我們的ATMType接口也遵循StateType接口。這也等同於ATMType中同樣聲明了這些動作,這些動作也是ATM機必須實現的。ATM機的類則遵循與ATMType協議。BaseStateClass是所有狀態的基類,該狀態的基類依賴於ATMType接口,因為在狀態中執行一些動作時會修改ATM機的狀態,所以BaseStateClass會依賴於ATMType協議。
所有的狀態類都繼承自BaseStateClass,同樣給出了該狀態下的一些特定動作。在一些狀態下執行一些動作時會修改ATM的動作。比如在ATM無卡狀態下調用插卡動作(insertBankCard())方法,那么ATM機的狀態就會懂無卡狀態變為有卡狀態。ATM機類的動作是依賴於狀態的,所以ATM機依賴於狀態的接口,而不依賴於狀態的具體實現。下方就是引入狀態模式的類圖。
2. 代碼實現
有了類圖在給出代碼實現則簡單許多,首先我們會相關的接口和基類。因為我們要面向接口編程,而不是面向實現編程,所以在程序設計之初我們要先定義接口。下方就是我們定義的StateType接口和ATMType接口,以及BaseStateClass基類。StateType接口定義了所有具體的狀態必須要實現的方法。而ATMType協議繼承自StateType,在StateType的基礎上添加了ATM機特有的方法,ATMType中聲明的方法也是ATM具體實現類中必須要實現的方法。代碼實現具體如下所示:
下方具體給出了無卡狀態類的實現方式,在無卡狀態下如果你插入銀行卡,也就是調用insertBankCard()方法,那么ATM機的狀態就會改變成“有卡狀態”,具體實現方式如下。在無卡狀態下,調用insertBankCard()以外的方法則不會改變ATM()機的狀態。其余的狀態的實現方式與無卡狀態類的實現方式類似,就是在合適的動作中改變ATM機對象的狀態。因篇幅有限,在本篇博客中就不給出具體實現了,具體實現請參加github上分享的完整實例。
接下來就是要重構我們的ATM機類了,ATM類要遵循ATMType協議。其中的stateObject成員變量的類型就是stateType類型的對象,該對象就是ATM機當前所處的狀態對象,該狀態對象模式是NoBankCardState類的對象。在ATM類中的動作是調用stateObject對象中相應的方法,因為狀態對象中已經封裝了該狀態下所有的動作,所以在ATM類中直接調用即可。如果ATM機狀態被改變了,那么stateObject所執行的動作也會不同,這也就是常說的多態了。下方的changeState()方法其實可以提取出類封成一個簡單工廠的,因為在此我們的主題是“狀態模式”,所以在此就沒有進行封裝。ATM類的具體實現方式如下。
3、測試用例
重構后的測試用例參照上一部分的測試用例,因為對外的類名以及動作沒有發生變化,所以之前的測試用例還是可以使用的。這也就是我們之前所說的重構的魅力,所以在此的測試用例就省略了。
至此,我們的狀態模式就介紹完了。狀態模式其實就是封裝了基於狀態的行為,並將行為委托到當前狀態中。有時候換一種思路會起到不一樣的效果。第一部分是動作包含各種狀態,而重構時我們使用了狀態包括不同動作的方式引入了“狀態模式”。因為博客中的代碼是部分代碼,完整實例請看github上分享的代碼.
上述代碼gitHub分享地址為:https://github.com/lizelu/DesignPatterns-Swift