Java實現有限狀態機的推薦方案分享
一、背景
平時工作開發過程中,難免會用到狀態機(狀態的流轉)。
如獎學金審批流程、請假審批流程、競標流程等,都需要根據不同行為轉到不同的狀態。
下面是一個簡單的模擬狀態機:
有些同學會選擇將狀態定義為常量,使用 if else 來流轉狀態,不太優雅。
有些同學會考慮將狀態定義為枚舉。
但是定義為枚舉之後,大多數同學會選擇使用 switch 來流轉狀態:
import lombok.Getter; public enum State { STATE_A("A"), STATE_B("B"), STATE_C("C"), STATE_D("D"); @Getter private final String value; State(String value) { this.value = value; } public static State getByValue(String value) { for (State state : State.values()) { if (state.getValue().equals(value)) { return state; } } return null; } /** * 批準後的狀態 */ public static State getApprovedState(State currentState) { switch (currentState) { case STATE_A: return STATE_B; case STATE_B: return STATE_C; case STATE_C: return STATE_D; case STATE_D: default: throw new IllegalStateException("當前已終態"); } } /** * 拒絕後的狀態 */ public static State getRejectedState(State currentState) { switch (currentState) { case STATE_A: throw new IllegalStateException("當前狀態不支持拒絕"); case STATE_B: case STATE_C: case STATE_D: default: return STATE_A; } } }
上面這種寫法有幾個弊端:
(1) getByValue 每次獲取枚舉值都要循環一次當前枚舉的所有常量,時間復雜度是
O(N),雖然耗時非常小,但總有些別扭,作為有追求的程序員,應該盡量想辦法優化掉。
(2) 總感覺使用 switch-case 實現狀態流轉,更多的是面向過程的產物。雖然可以實現功能,但沒那麼“面向對象”,既然 State 枚舉就是用來表示狀態,如果同意和拒絕可以通過 State 對象的方法獲取就會更直觀一些。
二、推薦方式
2.1 自定義的枚舉
通常狀態流轉有兩種方向,一種是贊同,一種是拒絕,分別流向不同的狀態。
由於本文討論的是有限狀態,我們可以將狀態定義為枚舉比較契合,除非初態和終態,否則贊同和拒絕都會返回一個狀態。
下面隻是一個DEMO, 實際編碼時可以自由發揮。
該 Demo 的好處是:
1 使用 CACHE緩存,避免每次通過 value 獲取 State都循環 State 枚舉數組
2 定義【同意】和【拒絕】抽象方法,每個 State 通過實現該方法來流轉狀態。
3 狀態的定義和轉換都收攏在一個枚舉中,更容易維護
雖然代碼看似更多一些,但是更“面向對象”一些。
package basic; import lombok.Getter; import java.util.Arrays; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; public enum State { /** * 定義狀態,並實現同意和拒絕的流轉 */ STATE_A("A") { @Override State getApprovedState() { return STATE_B; } @Override State getRejectedState() { throw new IllegalStateException("STATE_A 不支持拒絕"); } }, STATE_B("B") { @Override State getApprovedState() { return STATE_C; } @Override State getRejectedState() { return STATE_A; } }, STATE_C("C") { @Override State getApprovedState() { return STATE_D; } @Override State getRejectedState() { return STATE_A; } }, STATE_D("D") { @Override State getApprovedState() { throw new IllegalStateException("當前已終態"); } @Override State getRejectedState() { return STATE_A; } }; @Getter private final String value; State(String value) { this.value = value; } private static final Map<String, State> CACHE; static { CACHE = Arrays.stream(State.values()).collect(Collectors.toMap(State::getValue, Function.identity())); } public static State getByValue(String value) { return CACHE.get(value); } /** * 批準後的狀態 */ abstract State getApprovedState(); /** * 拒絕後的狀態 */ abstract State getRejectedState(); }
測試代碼
package basic; import static basic.State.STATE_B; public class StateDemo { public static void main(String[] args) { State state = State.STATE_A; // 一直贊同 State approvedState; do { approvedState = state.getApprovedState(); System.out.println(state + "-> approved:" + approvedState); state = approvedState; } while (state != State.STATE_D); // 獲取某個狀態的贊同和拒絕後的狀態 System.out.println("STATE_B approved ->" + STATE_B.getApprovedState()); System.out.println("STATE_C reject ->" + State.getByValue("C").getRejectedState()); System.out.println("STATE_D reject ->" + State.getByValue("D").getRejectedState()); } }
輸出結果:
STATE_A-> approved:STATE_B
STATE_B-> approved:STATE_C
STATE_C-> approved:STATE_D
—–
STATE_B approved ->STATE_C
STATE_C reject ->STATE_A
STATE_D reject ->STATE_A
本質上通過不同的方法調用實現自身的流轉,而且贊同和拒絕定義為抽象類,可以“強迫”讓狀態的定義方明確自己的狀態流轉。
整體邏輯比較內聚,狀態的定義和流轉都在 State 類中完成。
2.2 外部枚舉
假如該枚舉是外部提供,隻提供枚舉常量的定義,不提供狀態流轉,怎麼辦?
我們依然可以采用 switch 的方式實現狀態流轉:
import static basic.State.*; public class StateUtils { /** * 批準後的狀態 */ public static State getApprovedState(State currentState) { switch (currentState) { case STATE_A: return STATE_B; case STATE_B: return STATE_C; case STATE_C: return STATE_D; case STATE_D: default: throw new IllegalStateException("當前已經是終態"); } } /** * 拒絕後的狀態 */ public static State getRejectedState(State currentState) { switch (currentState) { case STATE_A: throw new IllegalStateException("當前狀態不支持拒絕"); case STATE_B: case STATE_C: case STATE_D: default: return STATE_A; } } }
還有更通用、更容易理解的編程方式呢(不用 switch)?
狀態機的每次轉換是一個 State 到另外一個 State 的映射,每個狀態都應該維護贊同和拒絕後的下一個狀態。
因此,我們很容易會聯想到使用【鏈表】來存儲這種關系 。
由於這裡是外部枚舉,無法將狀態流轉在枚舉內部完成(定義),就意味著我們還需要自定義狀態節點來表示流轉,如:
import lombok.Data; @Data public class StateNode<T> { private T state; private StateNode<T> approveNode; private StateNode<T> rejectNode; }
這樣構造好鏈表以後,還需在工具類中要構造 State 到 StateNode 的映射(因為對於外部來說,隻應該感知 State 類,不應該再去理解 StateNode ) , 提供贊同和拒絕方法,內部通過拿到贊同和拒絕對應的 StateNode 之後拿到對應的 State 返回即可。
偽代碼如下:
public class StateUtils{ // 構造 StateNode 鏈表,和構造 cache Map 略 private Map<State, StateNode<State>> cache ; public State getApproveState(State current){ StateNode<State> node = cache.get(current); return node == null? null: return node.getApproveNode().getState(); } public State getRejectState(State current){ StateNode<State> node = cache.get(current); return node == null? null: return node.getRejectNode().getState(); } }
整體比較曲折,不如直接將贊同和拒絕定義在 State 枚舉內更直觀。
下面給出一種 “狀態鏈模式” 的解決方案。
贊同和拒絕底層分別使用兩個 Map 存儲。
為瞭更好地表達每次狀態的方向(即 Map 中的 key 和 value),每一個映射定義為 from 和 to 。
為瞭避免隻有 from 沒有 to ,定義一個中間類型 SemiData,隻有調用 to 之後才可以繼續鏈式編程下去,最終構造出狀態鏈。
以下結合 Map 的數據結構,結合升級版的 Builder 設計模式,實現鏈式編程:
package basic; import java.util.HashMap; import java.util.Map; public class StateChain<T> { private final Map<T, T> chain; private StateChain(Map<T, T> chain) { this.chain = chain; } public T getNextState(T t) { return chain.get(t); } public static <V> Builder<V> builder() { return new Builder<V>(); } static class Builder<T> { private final Map<T, T> data = new HashMap<>(); public SemiData<T> from(T state) { return new SemiData<>(this, state); } public StateChain<T> build() { return new StateChain<T>(data); } public static class SemiData<T> { private final T key; private final Builder<T> parent; private SemiData(Builder<T> builder, T key) { this.parent = builder; this.key = key; } public Builder<T> to(T value) { parent.data.put(key, value); return parent; } } } }
使用案例:
package basic; import static basic.State.*; public class StateUtils { private static final StateChain<State> APPROVE; private static final StateChain<State> REJECT; static { APPROVE = StateChain.<State>builder().from(STATE_A).to(STATE_B).from(STATE_B).to(STATE_C).from(STATE_C).to(STATE_D).build(); REJECT = StateChain.<State>builder().from(STATE_B).to(STATE_A).from(STATE_C).to(STATE_A).from(STATE_D).to(STATE_A).build(); } /** * 批準後的狀態 */ public static State getApprovedState(State currentState) { State next = APPROVE.getNextState(currentState); if(next == null){ throw new IllegalStateException("當前已經終態"); } return next; } /** * 拒絕後的狀態 */ public static State getRejectedState(State currentState) { State next = REJECT.getNextState(currentState); if(next == null){ throw new IllegalStateException("當前狀態不支持駁回"); } return next; } }
測試方法
import static basic.State.STATE_B; public class StateDemo { public static void main(String[] args) { State state = State.STATE_A; // 一直贊同 State approvedState; do { approvedState = StateUtils.getApprovedState(state); System.out.println(state + "-> approved:" + approvedState); state = approvedState; } while (state != State.STATE_D); System.out.println("-------"); // 獲取某個狀態的贊同和拒絕後的狀態 System.out.println("STATE_B approved ->" + StateUtils.getApprovedState(STATE_B)); System.out.println("STATE_C reject ->" + StateUtils.getRejectedState(State.getByValue("C"))); System.out.println("STATE_D reject ->" + StateUtils.getRejectedState(State.getByValue("D"))); } }
輸出結果
STATE_A-> approved:STATE_B
STATE_B-> approved:STATE_C
STATE_C-> approved:STATE_D
—-
STATE_B approved ->STATE_C
STATE_C reject ->STATE_A
STATE_D reject ->STATE_A
這種方式更加靈活,可定義多條狀態鏈,實現每個鏈的狀態各自流轉。而且性能非常好。
巧妙地將狀態的轉換定義和 Map 的定義合二為一,既能夠表意(from,to 比較明確),又能獲得很好的性能(獲取贊同和拒絕後的狀態轉化為
通過 key 取 Map 中的 value ),還有不錯的編程體驗(鏈式編程)。
以上隻是 DEMO,實際編碼時,可自行優化。
可能還有一些開源的包提供狀態機的功能,但核心原理大同小異。
三、總結
本文結合自己的理解,給出一種推薦的有限狀態機的寫法。
給出瞭自有狀態枚舉和外部狀態枚舉的解決方案,希望對大傢有幫助。
通過本文,大傢也可以看出,簡單的問題深入思考,也可以得到不同的解法。
希望大傢不要滿足現有方案,可以靈活運用所學來解決實踐問題。
到此這篇關於Java實現有限狀態機的推薦方案的文章就介紹到這瞭,更多相關Java實現有限狀態機內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Java 枚舉類和自定義枚舉類和enum聲明及實現接口的操作
- java ImmutableMap的使用說明
- 全面解析java final關鍵字
- Java中lombok的@Builder註解的解析與簡單使用詳解
- Java創建型模式之建造者模式詳解