(本文由言念小文原創,轉載請注明出處)
在實際工作中經常遇到某個對象,處於不同的狀態有不同行為邏輯、且狀態之間可以相互遷移的業務場景,特別是在開發通信協議棧類軟件中尤為多見。《設計模式之禪》這本書中對狀態模式有着非常詳盡的講解(目前為止我認為講解得最好的書),但總覺得自己沒能夠理解透徹、靈活運用。直到今年完成了一個通信協議軟件的開發,重新研究了“狀態機”,然后回過頭來理解當初學習的狀態模式,豁然開朗。因此,本文先從狀態機開始講解,然后結合狀態機詳細闡述狀態模式的兩種實現方式,最后給出狀態模式的優缺點及其使用場景。
一 案例描述
按照老風格,本文先描述一個場景案例,然后圍繞案例來展開后文。相信每個人都用過手機的應用商城,通常在應用商城中會將可以安裝的app以列表(listview)的形式呈現,一個應用占據列表的一個子項(item),如下圖1所示:
圖1
我們將注意力聚焦到item的按鈕上:
a當檢測到可安裝的app,按鈕顯示“安裝”;
b點擊按鈕,軟件會去下載app安裝包,這時按鈕更新視圖,顯示“正在下載”(即安裝進度);
c下載完成后,軟件自動安裝app,按鈕顯示“正在安裝”;
d安裝完成后,按鈕顯示“打開”,這時點擊按鈕將打開對應的app。
通常,一切順利,我們安裝一個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 應用場景
個人認為,對於某個模塊或者對象,其行為出現狀態機特征的均可以使用該模式,以達到解耦、高擴展性、避免過多條件分支語句的目的。