(本文由言念小文原創,轉載請註明出處)設計模式
在實際工做中常常遇到某個對象,處於不一樣的狀態有不一樣行爲邏輯、且狀態之間能夠相互遷移的業務場景,特別是在開發通訊協議棧類軟件中尤其多見。《設計模式之禪》這本書中對狀態模式有着很是詳盡的講解(目前爲止我認爲講解得最好的書),但總以爲本身沒可以理解透徹、靈活運用。直到今年完成了一個通訊協議軟件的開發,從新研究了「狀態機」,而後回過頭來理解當初學習的狀態模式,豁然開朗。所以,本文先從狀態機開始講解,而後結合狀態機詳細闡述狀態模式的兩種實現方式,最後給出狀態模式的優缺點及其使用場景。架構
一 案例描述app
按照老風格,本文先描述一個場景案例,而後圍繞案例來展開後文。相信每一個人都用過手機的應用商城,一般在應用商城中會將能夠安裝的app以列表(listview)的形式呈現,一個應用佔據列表的一個子項(item),以下圖1所示:ide
圖1學習
咱們將注意力聚焦到item的按鈕上:this
a當檢測到可安裝的app,按鈕顯示「安裝」;編碼
b點擊按鈕,軟件會去下載app安裝包,這時按鈕更新視圖,顯示「正在下載」(即安裝進度);spa
c下載完成後,軟件自動安裝app,按鈕顯示「正在安裝」;設計
d安裝完成後,按鈕顯示「打開」,這時點擊按鈕將打開對應的app。3d
一般,一切順利,咱們安裝一個app,按鈕會經歷「安裝」「正在下載」「正在安裝」「打開」四種狀態。惋惜的是,每每事有多磨:
當下載app安裝包時,可能出現下載異常,這時按鈕切換狀態到「下載失敗」,點擊按鈕,軟件從新嘗試下載,按鈕切換狀態到「正在下載」;
當安裝app時,可能出現安裝失敗,這時按鈕切換狀態到「安裝失敗」,點擊按鈕,軟件從新嘗試安裝,按鈕切換狀態到「正在安裝」;
以上,即是咱們更新一個軟件時,可能遇到狀況。後文,咱們將實現上述功能的軟件模塊稱爲「app安裝模塊」,本文將以這個案例爲基礎,圍繞實現「app安裝模塊」展開狀態機和狀態模式的講解。
二 狀態機
1.什麼是狀態機
一般咱們工做中接觸到的狀態機都是有限狀態機,那麼什麼是有限狀態機呢?偷個懶直接百度大挪移:
有限狀態機,(英語:Finite-state machine, FSM),又稱有限狀態自動機,簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉移和動做等行爲的數學模型。
注意這個定義裏面兩個關鍵字「有限狀態」「自動機」。「有限狀態」指明狀態機中的狀態是有限且明確的,在案例中button的狀態有:待安裝、正在下載、下載失敗、正在安裝、安裝失敗、待打開。「自動機」說明狀態機中:狀態及狀態下對應動做是在狀態機內部自動轉換和執行的,調用狀態機的客戶端,無需關心狀態機內部的狀態遷移和動做執行。
2.1 狀態機的構成要素
狀態機由如下四大要素構成:
現態(Qn) -- 當前狀態機所處的狀態。
次態(Qn+1) -- 狀態機要遷移到的新狀態。
事件(EVENT)(又稱爲條件) -- 狀態機的觸發信號;事件到來,可以觸發狀態機執行特定動做,或進行狀態遷移,或兩者皆執行;事件通常來自於狀態機外部。
動做(ACTION) -- 事件到來後,狀態機執行的動做,動做執行完後,狀態機可遷移到新狀態也可維持原狀態,故而對於狀態機中的某一狀態,動做並不是必須。
2.2 狀態機的描述方式
狀態機的描述方式有兩種:狀態遷移圖和狀態機表。
狀態遷移圖:
狀態遷移圖經過圖形的方式來描述對象的所有狀態邏輯,這種方式比較直觀、清晰。狀態遷移圖由狀態、狀態遷移、事件和動做構成。其中,事件和動做寫在狀態遷移的帶箭頭線條上,如圖2所示,圖2爲「app安裝模塊」狀態遷移圖,圓圈和雙圓圈表示起始和結束狀態。
圖2 「app安裝」狀態遷移圖
狀態機表:
狀態遷移表經過矩陣的方式,描述狀態機的狀態遷移與行爲邏輯。狀態機表有兩種寫法:
第一種,橫豎表頭都爲狀態,橫表頭爲現態,豎表頭爲次態,現態和次態相交的單元格爲事件觸發後要執行的動做,如表1所示:
現態 次態 |
待安裝 |
下載中 |
下載失敗 |
安裝中 |
安裝失敗 |
待打開 |
待安裝 |
- |
- |
- |
- |
- |
- |
下載中 |
download() |
- |
download() |
- |
- |
- |
下載失敗 |
- |
undo() |
- |
- |
- |
- |
安裝中 |
install() |
install() |
- |
- |
install() |
- |
安裝失敗 |
- |
- |
- |
undo() |
- |
- |
待打開 |
- |
- |
- |
undo() |
- |
open() |
表1
注意1:途中undo()爲表示只作狀態轉移,實際不執行其餘動做。
注意2:因爲不論什麼狀態,只要狀態遷移了,都會有UI上變化,所以更新UI的動做updateView()不重複的提現啊狀態機圖和狀態機表中。
第二種,橫表頭爲現態,豎表頭爲事件觸發後的動做,現態和動做相交的單元格,爲次態。
現態 動做 |
待安裝 |
下載中 |
下載失敗 |
安裝中 |
安裝失敗 |
待打開 |
download() |
下載中 |
- |
下載中 |
- |
- |
- |
install() |
安裝中 |
安裝中 |
- |
- |
安裝中 |
- |
open() |
- |
- |
- |
待打開 |
- |
待打開 |
表2
兩種狀態機表各有特色,第一種比較適合狀態較多的狀況,第二種適合動做比較多的狀況(根據小文的我的工程經驗,比較推薦第二種)。從狀態表中能夠看到,狀態表不能很好的描述出單個狀態和動做的觸發事件,所以一般狀態表仍是須要和狀態遷移圖結合使用的。
2.1 狀態機的運行過程
狀態機的運行實際是狀態的遷移和對應動做的執行,這裏我總結以下的運行分支:
EVENT-->ACTION // 事件觸發,只執行動做,不轉移狀態
EVENT-->TRANS STATE // 事件觸發,只轉移狀態,不執行動做
EVENT-->ACTION-->TRANS STATE // 事件觸發,先作動做,後轉移狀態
須要說明的是:這裏的ACTION一般都是觸發狀態轉移必需要作的動做,若是不作,狀態將沒法成功遷移。好比,案例「app安裝模塊」從「安裝」到「正在下載」狀態的遷移,狀態遷移前必需要執行download()動做,若是沒有執行這個動做,狀態是沒法成功遷移的。
有人可能會有疑問,能夠按照EVENT-->TRANS STATE-->ACTION運行嗎,其實在實際的編碼過程當中,是能夠的:案例中「app安裝模塊」先從「安裝」遷移到「正在下載」狀態,緊接着在「正在下載」狀態下執行download()動做,這樣在功能實現上與前一種運行順序沒有差別。不過,我我的更喜歡EVENT-->ACTION-->TRANS STATE這種順序,由於這種順序更加符合咱們的天然邏輯。
三 狀態模式
1 爲何要使用狀態模式?
1.1 什麼是狀態模式?
在此,我不想套用GOF的定義,由於定義每每是總結和歸納後高度提煉的概念,不太利於理解。當咱們在項目開發過程當中,分析某些業務對象或模塊,發現他們的運行規律表現爲狀態機特徵的時候,狀態模式可能就要提上咱們架構方案了。那麼到底什麼是狀態模式呢?別急,看完後面的文章,相信你本身能總結出來。
1.2 爲何要使用狀態模式
咱們使用狀態模式就是爲了用軟件實現具備狀態機特徵的業務對象,爲何要這樣作呢?在狀態機的定義一節,咱們講到「狀態及狀態下對應動做是在狀態機內部自動轉換和執行的,調用狀態機的客戶端,無需關心狀態機內部的狀態遷移和動做執行」。所以,狀態模式是一種高度封裝、高度解耦的、易於拓展的架構模式。這麼好的模式,固然要啦。
2 使用狀態模式完成案例
咱們先來分解一下狀態模式要達到的目標:a.狀態及狀態下對應動做是在狀態機內部自動轉換和執行的;b.調用狀態機的客戶端,無需關心狀態機內部的狀態遷移和動做執行。
要達到目標a,那麼咱們每種具體狀態必需要進行封裝:狀態內部的動做和轉換,是要封裝在這個狀態內部的,每一個狀態都必須至少要將如下兩個要素封裝其中:動做、狀態轉移方法。
要實現目標b,具體的各類狀態就不能直接暴露給調用的客戶端(Client),從Client到各具體狀態(ConcreteState),中間必需要有一個對象,對各狀態進行統一管理和無差異的暴露給Client,Client只須要與這個對象交互,就能觸發軟件模塊自動正確運行。
2.1靜態類圖
經過目標分解,而後反向推理分析,即可以直接給出靜態類圖方案:
Client:調用「app安裝模塊」的客戶端。
StateContext:狀態上下文,即狀態的環境類,對各個具體狀態進行封裝和管理,讓各個具體狀態無差異的曝露給客戶端,StateContext曝露給客戶端的永遠是當前的狀態。
State:抽象狀態。
ConcreteState:具體狀態。
2.2狀態模式實現代碼
2.2.1方式1
方式1按照咱們常規的天然邏輯,在各個狀態中按照EVENT-->ACTION-->TRANS STATE順序運行。
第一步 定義抽象State
public abstract class State { public StateContext stateContext; public void setStateContext(StateContext context){ stateContext = context; } protected void updateView(String s) { System.out.println("update button view = " + s); }; protected abstract void doAction(Event e); protected abstract void transState(Event e); public abstract void eventChange(Event e); }
對於狀態,必需要有事件觸發、執行動做、狀態轉移幾種方法,結合本案例,還要有更新UI的方法,此外一個狀態必需要持有狀態環境對象stateContext,才能在狀態遷移的時候,更新stateContext中的當前狀態。
第二步 定義StateContext
public class StateContext { // 當前狀態 public State currState; // 定義出全部狀態 public static final StateToInstall stateToInstall = new StateToInstall(); public static final StateDownloading stateDownloading = new StateDownloading(); public static final StateDownloadFailed stateDownloadFailed = new StateDownloadFailed(); public static final StateInstalling stateInstalling = new StateInstalling(); public static final StateInstallFailed stateInstallFailed = new StateInstallFailed(); public static final StateToOpen stateToOpen = new StateToOpen(); public StateContext(State state) { currState = state; // context對象傳遞給當前狀態對象 this.currState.setStateContext(this); } /** * 獲取當前狀態 * @return 當前狀態 */ public State getCurrState() { return currState; } /** * 設置當前狀態 * @param currState */ public void setCurrState(State currState) { this.currState = currState; // context對象傳遞給當前狀態對象 this.currState.setStateContext(this); } /** * 觸發條件改變 * @param e */ public void eventChange(Event e) { currState.eventChange(e); } }
注意:StateContext與State之間是聚合關係,故而在StateContext中定義出全部具體狀態。
第三步 定義事件類型
這裏用一個枚舉類來定義觸發「app安裝模塊」動做執行和狀態遷移的事件信號
public enum Event { EVENT_CLICK, // 按鈕點擊 EVENT_DOWNLOAD_FAILED, // 下載失敗 EVENT_DOWNLOAD_SUCCESS, // 如今成功 EVENT_INSTALL_FAILED, // 安裝失敗 EVENT_INSTALL_SUCCESS, // 安裝成功 }
第四步 定義具體狀態類
/** * 「待安裝」狀態類 * @author 言念小文 * */ public class StateToInstall extends State{ @Override protected void doAction(Event e) { if(Event.EVENT_CLICK.equals(e) && !checkDownloaded()) { System.out.println("current state = StateToInstall, " + "event change signal = click button, " + "do action download()"); updateView("下載中"); return; } if(Event.EVENT_CLICK.equals(e) && checkDownloaded()) { System.out.println("current state = StateToInstall, " + "event change signal = click button, " + "do action install()"); updateView("安裝中"); return; } } @Override protected void transState(Event e) { if(Event.EVENT_CLICK.equals(e) && !checkDownloaded()) { System.out.println("current state = StateToInstall, " + "event change signal = click button, " + "transfer state to StateDownloading"); // 狀態轉移後,設置狀態環境類當前狀態 stateContext.setCurrState(StateContext.stateDownloading); return; } if(Event.EVENT_CLICK.equals(e) && checkDownloaded()) { System.out.println("current state = StateToInstall, " + "event change signal = click button, " + "transfer state to StateInstalling"); // 狀態轉移後,設置狀態環境類當前狀態 stateContext.setCurrState(StateContext.stateInstalling); return; } } @Override public void eventChange(Event e) { if(!Event.EVENT_CLICK.equals(e)) { return; } // 執行動做 doAction(e); // 轉移狀態 transState(e); } private boolean checkDownloaded() { return false; } }
在具體類中實現doAction(Event e)、transState(Event e)、eventChange(Event e)方法,具體類持有環境對象StateContext的實例。當外部事件信號經過StateContext傳入某個具體類中,StateContext調用具體類中的eventChange(Event e)方法,eventChange(Event e)方法經過調用doAction()和transState()來實現動做的執行和狀態的轉移,這樣具體類就將本狀態執行的動做和狀態遷移所有封裝在具體狀態類中,Client只須要調用StateContext實例,而無需關心具體的狀態類。
/** * 「下載中」狀態類 * @author 言念小文 * */ public class StateDownloading extends State{ @Override protected void doAction(Event e) { // 不管是下載成功或失敗,無需執行其餘動做,緊更新view if(Event.EVENT_DOWNLOAD_FAILED.equals(e)) { System.out.println("current state = StateDownloading, " + "event change signal = download failed, " + "do action nothing"); updateView("下載失敗"); return; } if(Event.EVENT_DOWNLOAD_SUCCESS.equals(e)) { System.out.println("current state = StateDownloading, " + "event change signal = download success, " + "do action install()"); updateView("安裝中"); return; } } @Override protected void transState(Event e) { if(Event.EVENT_DOWNLOAD_FAILED.equals(e)) { System.out.println("current state = StateToInstall, " + "event change signal = click button, " + "transfer state to StateDownloadFailed"); // 狀態轉移後,設置狀態環境類當前狀態 stateContext.setCurrState(StateContext.stateDownloadFailed); return; } if(Event.EVENT_DOWNLOAD_SUCCESS.equals(e)) { System.out.println("current state = StateToInstall, " + "event change signal = click button, " + "transfer state to StateInstalling"); // 狀態轉移後,設置狀態環境類當前狀態 stateContext.setCurrState(StateContext.stateInstalling); return; } } @Override public void eventChange(Event e) { if(!Event.EVENT_DOWNLOAD_FAILED.equals(e) && !Event.EVENT_DOWNLOAD_SUCCESS.equals(e)) { return; } // 執行動做 doAction(e); // 轉移狀態 transState(e); } } /** * 「下載失敗」狀態類 * @author 言念小文 * */ public class StateDownloadFailed extends State{ @Override protected void doAction(Event e) { if(Event.EVENT_CLICK.equals(e)) { System.out.println("current state = StateDownloadFailed, " + "event change signal = click button, " + "do action download()"); updateView("下載中"); return; } } @Override protected void transState(Event e) { if(Event.EVENT_CLICK.equals(e)) { System.out.println("current state = StateDownloadFailed, " + "event change signal = click button, " + "transfer state to StateDownloading"); // 狀態轉移後,設置狀態環境類當前狀態 stateContext.setCurrState(StateContext.stateDownloading); return; } } @Override public void eventChange(Event e) { if(!Event.EVENT_CLICK.equals(e)) { return; } // 執行動做 doAction(e); // 轉移狀態 transState(e); } } /** * 「安裝中」狀態類 * @author 言念小文 * */ public class StateInstalling extends State{ @Override protected void doAction(Event e) { if(Event.EVENT_INSTALL_FAILED.equals(e)) { System.out.println("current state = StateInstalling, " + "event change signal = install failed, " + "do action nothing"); updateView("安裝失敗"); return; } if(Event.EVENT_INSTALL_SUCCESS.equals(e)) { System.out.println("current state = StateInstalling, " + "event change signal = install success, " + "do action nothing"); updateView("打開"); return; } } @Override protected void transState(Event e) { if(Event.EVENT_INSTALL_FAILED.equals(e)) { System.out.println("current state = StateInstalling, " + "event change signal = install failed, " + "transfer state to StateInstallFailed"); // 狀態轉移後,設置狀態環境類當前狀態 stateContext.setCurrState(StateContext.stateInstallFailed); return; } if(Event.EVENT_INSTALL_SUCCESS.equals(e)) { System.out.println("current state = StateInstalling, " + "event change signal = install success, " + "transfer state to StateToOpen"); // 狀態轉移後,設置狀態環境類當前狀態 stateContext.setCurrState(StateContext.stateToOpen); return; } } @Override public void eventChange(Event e) { if(!Event.EVENT_INSTALL_FAILED.equals(e) && !Event.EVENT_INSTALL_SUCCESS.equals(e)) { return; } // 執行動做 doAction(e); // 轉移狀態 transState(e); } } /** * 「安裝失敗」狀態類 * @author 言念小文 * */ public class StateInstallFailed extends State{ @Override protected void doAction(Event e) { if(Event.EVENT_CLICK.equals(e)) { System.out.println("current state = StateInstallFailed, " + "event change signal = click button, " + "do action install()"); updateView("安裝中"); return; } } @Override protected void transState(Event e) { if(Event.EVENT_CLICK.equals(e)) { System.out.println("current state = StateInstallFailed, " + "event change signal = click button, " + "transfer state to StateInstalling"); // 狀態轉移後,設置狀態環境類當前狀態 stateContext.setCurrState(StateContext.stateInstalling); return; } } @Override public void eventChange(Event e) { if(!Event.EVENT_CLICK.equals(e)) { return; } // 執行動做 doAction(e); // 轉移狀態 transState(e); } } /** * 「待打開」狀態類 * @author 言念小文 * */ public class StateToOpen extends State{ @Override protected void doAction(Event e) { if(Event.EVENT_CLICK.equals(e)) { System.out.println("current state = StateToOpen, " + "event change signal = click button, " + "do action open()"); // 點擊打開,button view沒有變化 updateView("打開"); return; } } @Override protected void transState(Event e) { if(Event.EVENT_CLICK.equals(e)) { // 狀態不發生轉移 stateContext.setCurrState(StateContext.stateToOpen); return; } } @Override public void eventChange(Event e) { if(!Event.EVENT_CLICK.equals(e)) { return; } // 執行動做 doAction(e); // 轉移狀態 transState(e); } }
第五步 定義Client,並運行程序
public class Client { public static void main(String[] args) { // 建立狀態環境類對象,並初始化狀態 StateContext context = new StateContext(StateContext.stateToInstall); // 下載 context.eventChange(Event.EVENT_CLICK); System.out.println("-----------------------------------------------\r"); // 下載失敗 context.eventChange(Event.EVENT_DOWNLOAD_FAILED); System.out.println("-----------------------------------------------\r"); // 從新下載 context.eventChange(Event.EVENT_CLICK); System.out.println("-----------------------------------------------\r"); // 下載成功 context.eventChange(Event.EVENT_DOWNLOAD_SUCCESS); System.out.println("-----------------------------------------------\r"); // 安裝失敗 context.eventChange(Event.EVENT_INSTALL_FAILED); System.out.println("-----------------------------------------------\r"); // 從新安裝 context.eventChange(Event.EVENT_CLICK); System.out.println("-----------------------------------------------\r"); // 安裝成功 context.eventChange(Event.EVENT_INSTALL_SUCCESS); System.out.println("-----------------------------------------------\r"); } }
Client類中,只須要持有StateContext,而後輸入不一樣的事件件號,就能夠出發「app安裝模塊」的動做執行和狀態遷移。
執行結果以下:
current state = StateToInstall, event change signal = click button, do action download()
update button view = 下載中
current state = StateToInstall, event change signal = click button, transfer state to StateDownloading
-----------------------------------------------
current state = StateDownloading, event change signal = download failed, do action nothing
update button view = 下載失敗
current state = StateToInstall, event change signal = click button, transfer state to StateDownloadFailed
-----------------------------------------------
current state = StateDownloadFailed, event change signal = click button, do action download()
update button view = 下載中
current state = StateDownloadFailed, event change signal = click button, transfer state to StateDownloading
-----------------------------------------------
current state = StateDownloading, event change signal = download success, do action install()
update button view = 安裝中
current state = StateToInstall, event change signal = click button, transfer state to StateInstalling
-----------------------------------------------
current state = StateInstalling, event change signal = install failed, do action nothing
update button view = 安裝失敗
current state = StateInstalling, event change signal = install failed, transfer state to StateInstallFailed
-----------------------------------------------
current state = StateInstallFailed, event change signal = click button, do action install()
update button view = 安裝中
current state = StateInstallFailed, event change signal = click button, transfer state to StateInstalling
-----------------------------------------------
current state = StateInstalling, event change signal = install success, do action nothing
update button view = 打開
current state = StateInstalling, event change signal = install success, transfer state to StateToOpen
-----------------------------------------------
2.2.2方式2
前文咱們說了,在實際編碼的過程當中,EVENT-->TRANS STATE-->ACTION執行順序也是能夠的的,只不過須要將Action定義在次態中,而後次態中的Action要委託到現態中執行。具體的編碼方式請參照《設計模式之禪道》關於狀態模式的章節。
四 狀態模式優缺點及使用場景
1 狀態模式優缺點
優勢:從前文咱們已經能夠看出,Client只須要持有StateContext實例,僅僅經過事件信號就能夠驅動「app安裝模塊」的運行,無需關注軟件模塊內部實現,故具備很好的封裝性,能很好解耦。若是要添加一種新狀態,只須要添加一個新的狀態子類,無需影響其餘類,故而擴展性良好。
缺點:隨着狀態增長,狀態子類會增多,致使類膨脹。
2 應用場景
我的認爲,對於某個模塊或者對象,其行爲出現狀態機特徵的都可以使用該模式,以達到解耦、高擴展性、避免過多條件分支語句的目的。