今天跟大家分享一個關於“狀態機”的話題。給你講清楚什麼是狀態機、為什麼需要狀態機、適用場景、有哪些具體的實現方案以及各個方案對比(附帶github源碼地址) ...
前言
今天跟大家分享一個關於“狀態機”的話題。狀態屬性在我們的現實生活中無處不在。比如電商場景會有一系列的訂單狀態(待支付、待發貨、已發貨、超時、關閉);員工提交請假申請會有申請狀態(已申請、審核中、審核成功、審核拒絕、結束);差旅報銷單會有單據審核狀態(已提交、審核中、審核成功、退回、打款中、打款成功、打款失敗、結束)等等。上述場景有一個共同問題:根據不同觸發條件執行不同處理動作最後落地不同的狀態。示例代碼如下:
Integer status=0;
if(condition1){
status=1;
}else if(condition2){
status=2;
}else if(condition3){
status=3;
}else if(condition4){
status=4;
}
複製代碼
那我們最容易能想到的自然是if-else方案。那if-else方案會有什麼問題呢?
主要有以下幾點:
- 複雜的業務流程,if.else代碼幾乎無法維護
- 隨著業務的發展,業務過程也需要變更及擴展,但if.else代碼段已經無法支持
- 沒有可讀性,變更風險特別大,可能會牽一發而動全身,線上事故層出不窮
- 其他業務邏輯可能也會跟if-else代碼塊耦合在一起,帶來更多的問題
狀態機的出現就是用來解決上述問題的。在複雜多狀態流轉情況下,通過狀態機的引入,我們希望相關代碼可讀性、擴展性能比if-else方案更好!
關於狀態機
▲什麼是狀態機
狀態機是有限狀態自動機的簡稱。有限狀態機(英語:finite-state machine,縮寫:FSM)又稱有限狀態自動機(英語:finite-state automaton,縮寫:FSA),簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉移和動作等行為的數學計算模型。
關於有限的解釋:也就是被描述的事物的狀態的數量是有限的,例如開關的狀態只有“開”和“關”兩個;燈的狀態只有“亮”和“滅”等等。
▲特點
一個狀態機可以具有有限個特定的狀態,它通常根據輸入,從一個狀態轉移到另一個狀態,不過也可能存在瞬時狀態,而一旦任務完成,狀態機就會立刻離開瞬時狀態。每個狀態根據不同的前置條件,會從當前狀態流轉至下一個狀態。
▲作用
使用狀態機來表達狀態的流轉,會使語義會更加清晰,會增強代碼的可讀性和可維護性。
▲適用場景
面對複雜的狀態流轉(一般是超過三個及以上的狀態流轉),那麼還是比較建議用狀態機來實現的。
各個狀態機方案
▲枚舉狀態機
Java中的枚舉是一個定義了一系列常量的特殊類(隱式繼承自class java.lang.Enum)。枚舉類型因為自身的線程安全性保障和高可讀性特性,是簡單狀態機的首選。
關於線程安全說明
我們隨便自定義一個枚舉:
public enum OpinionsEnum {
PASS,NOT_PASS
}
複製代碼
試著反編譯上述代碼:
public final class OpinionsEnum extends java.lang.Enum<OpinionsEnum> {
public static final OpinionsEnum PASS;
public static final OpinionsEnum NOT_PASS;
public static OpinionsEnum[] values();
public static OpinionsEnum valueOf(java.lang.String);
static {};
}
複製代碼
通過反編譯後的代碼我們看到:OpinionsEnum它繼承了java.lang.Enum類;class前的final標識告訴我們此枚舉類不能被繼承。
我們接著看它的兩個屬性:PASS、NOT_PASS。它們無一例外都經過了staic 的修飾,而我們知道staic修飾的屬性會在類被載入之後就完成初始化,而這個過程是線程安全的。
示例代碼:
public enum State {
SUBMIT_APPLY {
@Override
State transition(String checkcondition) {
System.out.println("員工提交請假申請單,同步流轉到部門經理審批 參數 = " + checkcondition);
return Department_MANAGER_AUDIT;
}
},
Department_MANAGER_AUDIT {
@Override
State transition(String checkcondition) {
System.out.println("部門經理審批完成,同步跳轉到HR進行審批 參數 = " + checkcondition);
return HR;
}
},
HR {
@Override
State transition(String checkcondition) {
System.out.println("HR完成審批,流轉到結束組件, 參數 = " + checkcondition);
return FINAL;
}
},
FINAL {
@Override
State transition(String checkcondition) {
System.out.println("流程結束, 參數 = " + checkcondition);
return this;
}
};
abstract State transition(String checkcondition);
}
複製代碼
public class StatefulObjectDemo {
private State state;
public StatefulObjectDemo() {
state = State.SUBMIT_APPLY;
}
public void performRequest(String checkCondition) {
state = state.transition(checkCondition);
}
public static void main(String[] args) {
StatefulObjectDemo theObject = new StatefulObjectDemo();
theObject.performRequest("arg1");
theObject.performRequest("arg2");
theObject.performRequest("arg3");
theObject.performRequest("arg4");
}
}
複製代碼
輸出:
員工提交請假申請單,同步流轉到部門經理審批 參數 = arg1
部門經理審批完成,同步跳轉到HR進行審批 參數 = arg2
HR完成審批,流轉到結束組件, 參數 = arg3
流程結束, 參數 = arg4
複製代碼
Java枚舉有一個比較有趣的特性即它允許為實例編寫方法,從而為每個實例賦予其行為。實現也很簡單,定義一個抽象的方法即可,這樣每個實例必須強制重寫該方法。(見示例的transition方法)
▲狀態模式實現的狀態機
是什麼
狀態模式是編程領域特有的名詞,是 23 種設計模式之一,屬於行為模式的一種。
它允許一個對象在其內部狀態改變時改變它的行為。對象看起來似乎修改了它的類。
作用狀態模式的設計意圖主要是為瞭解決兩個主要問題:
-
當一個對象的內部狀態改變時,它應該改變它的行為。
-
應獨立定義特定於狀態的行為。也就是說,添加新狀態不應影響現有狀態的行為。
類圖:
類圖
定義一個State介面,它可以有N個實現類,每個實現類需重寫介面State定義的handle方法。它還有一個Context上下文類,內部持有一個State對象引用,外部狀態發生改變(構造器內傳入不同實現類),最終實現類自身行為動作也接著改變(實現類調用其自身的handle方法)。
Context示意圖參考
用狀態模式實現的代碼示例:
public interface SwitchState {
void handle();
}
public class TurnOffAction implements SwitchState{
@Override
public void handle() {
System.out.println("關燈");
}
}
public class TurnOnAction implements SwitchState{
@Override
public void handle() {
System.out.println("開燈");
}
}
public class Context {
private SwitchState state;
public Context(SwitchState state){
this.state=state;
}
public void doAction(){
state.handle();
}
}
複製代碼
輸出
public class StatePatternDemo {
@DisplayName("狀態模式測試用例-開燈")
@Test
public void turnOn() {
Context context = new Context(new TurnOnAction());
context.doAction();
}
輸出:開燈
@DisplayName("狀態模式測試用例-關燈")
@Test
public void turnOff() {
Context context = new Context(new TurnOffAction());
context.doAction();
}
}
輸出:關燈
複製代碼
大家看下這段示例代碼:Context類有一個有參構造方法,參數類型是State,所以實例化對象的時候你可以傳入State的不同的實現類。最終context.doAction()調用的是不同實現類的doAction方法。
▲開源實現
目前開源的狀態機實現方案有spring-statemachine、squirrel-foundation、sateless4j等。其中spring-statemachine、squirrel-foundation在github上star和fock數穩居前二。
不過這些狀態機普通使用下來普遍存在兩個問題:
問題一:太複雜
因為基本囊括了UML State Machine上列舉的所有功能,功能是強大了,但也搞得體積過於龐大、臃腫、很重。很多功能實際生產場景中根本用不到。
支持的高階功能有:狀態的嵌套(substate),狀態的並行(parallel,fork,join)、子狀態機等等。大家可以對照一下這些功能你是否用的到。
問題二:性能差
這些狀態機都是有狀態的(Stateful)的,有狀態意味著多線程併發情況下如果是單個實例就容易出現線程安全問題。在如今的普遍分散式多線程環境中,你就不得不每次一個請求就創建一個狀態機實例。但問題來了一旦碰到某些狀態機它的構建過程很複雜,如果當下QPS又很高話,往往會造成系統的性能瓶頸。
在這裡我給大家推薦一款阿裡開源的狀態機:cola-statemachine。github地址:github.com/alibaba/COL…
作者(張建飛:阿裡高級技術專家)講到面對複雜的狀態流轉,當時他們團隊也想搞個狀態機來減負,經過深思熟慮、不斷類比之後他們考慮自研。希望能設計出一款功能相對簡單、性能良好的開源狀態機;最後命名為cola-component-statemachine(實現了內部DSL語法;目前最新版本:4.3.1)
示例代碼:
//構建一個狀態機(生產場景下,生產場景可以直接初始化一個Bean)
StateMachineBuilder<StateMachineTest.ApplyStates, StateMachineTest.ApplyEvents, Context> builder = StateMachineBuilderFactory.create();
//外部流轉(兩個不同狀態的流轉)
builder.externalTransition()
.from(StateMachineTest.ApplyStates.APPLY_SUB)//原來狀態
.to(StateMachineTest.ApplyStates.AUDIT_ING)//目標狀態
.on(StateMachineTest.ApplyEvents.SUBMITING)//基於此事件觸發
.when(checkCondition1())//前置過濾條件
.perform(doAction());//滿足條件,最終觸發的動作
複製代碼
上述代碼先構建了一個狀態機實例:from和to分別定義了源狀態和目標狀態,on定義了一個事件(狀態機基於事件觸發)當狀態機匹配到指定的事件後,會進行條件過濾,如果滿足指定條件,就會執行perform定義的動作函數,最終狀態會從from內的源狀態變成to定義的目標狀態。
我們一起來看看客戶端是怎麼觸發自定義的狀態機的:
複製代碼
StateMachine<StateMachineTest.ApplyStates, StateMachineTest.ApplyEvents, Context> stateMachine = builder.build("ChoiceConditionMachine");
//fireEvent發送一個事件;對應上面示例代碼的ApplyEvents.SUBMITING.
StateMachineTest.ApplyStates target1 = stateMachine.fireEvent(StateMachineTest.ApplyStates.APPLY_SUB, StateMachineTest.ApplyEvents.SUBMITING, new Context("pass"));
輸出:
from:APPLY_SUB to:AUDIT_ING on:SUBMITING condition:pass
複製代碼
我把上述三款狀態機的示例代碼都放在了github上,有興趣的小伙伴可以自行查閱。
github地址:
總結
好了,此篇文章即將進入尾聲,讓我們一起來做個總結。
為什麼引入狀態機?
前言部分我也提到了在面對複雜的狀態流轉場景下if-else方案主要容易引起可讀性、可擴展、易出錯等問題,所以引入狀態機主要為了降低這些風險。
狀態機的實現方案對比:
狀態機實現方案我舉例了Java枚舉、狀態模式、開源狀態機等幾個實現方案。狀態模式的問題是它需要定義介面、和實現類還附帶一個Context上下文類,編碼層面比較複雜。Java枚舉版的狀態機主要問題是擴展粒度不夠基本都是線性擴展,封裝在一個類中,太複雜的狀態流轉這個類也會變得臃腫不堪,維護性變低。
所以也推薦了一款比較理想的開源狀態機實現--cola-component-statemachine。它使用相當簡單,因為實現了內部DSL,所以可讀性很強,當然擴展性也比較不錯。
公眾號:
裡面不僅彙集了硬核的乾貨技術、還彙集了像左耳朵耗子、張朝陽總結的高效學習方法論、職場升遷竅門、軟技能。希望能輔助你達到你想夢想之地!
公眾號內回覆關鍵字“電子書”下載pdf格式的電子書籍(併發編程、JVM、MYSQL、JAVAEE、Linux、Spring、分散式等,你想要的都有!)、“開發手冊”獲取阿裡開發手冊2本、"面試"獲取面試PDF資料。