系統設計 - 使用有限狀態機優化應用狀態管理
公衆號:TechLead 少個分號
知乎:少個分號
微信號:shaogefenhao
網站:shaogefenhao.com
狀態機是一個非常有用的設計模式,可以大大簡化使用應用代碼對複雜狀態管理、校驗、流轉時的複雜性。用好狀態機,可以讓代碼更加精緻,也更簡潔。
01 爲什麼需要狀態機?
假設我們正在開發一個電子商務平臺的訂單處理系統。訂單通常會經歷多個狀態,例如:
-
新訂單
-
已支付
-
已發貨
-
已交付
-
已取消
在這個場景中,訂單狀態的變化必須符合業務規則,比如:
-
新訂單可以被取消或支付。
-
已支付的訂單可以被髮貨或取消。
-
已發貨的訂單可以被交付。
如果用 Java 代碼來寫的話,每一次狀態變化都需要校驗一次。如果不使用狀態機,可能會通過多個 if-else 或 switch-case 語句來管理這些狀態轉換。這種方式不僅代碼繁瑣,而且隨着業務邏輯的複雜化,會導致代碼難以維護。
例如:
public class Order {
private OrderState state;
public Order() {
this.state = OrderState.NEW;
}
public void pay() {
if (state == OrderState.NEW) {
state = OrderState.PAID;
} else {
throw new IllegalStateException("Order cannot be paid in state: " + state);
}
}
public void ship() {
if (state == OrderState.PAID) {
state = OrderState.SHIPPED;
} else {
throw new IllegalStateException("Order cannot be shipped in state: " + state);
}
}
// 下面同理不浪費篇幅了
}
我們可以把狀態控制的部分抽出來,然後提前約定一個流轉規則就行了,實現這個流轉規則的代碼我們就成爲狀態機。我們可以用 UML 把上面的狀態流轉,繪製成狀態圖(使用 PlantUML 繪製)。
狀態機的本質是把流程控制邏輯和普通業務邏輯做分離。如果流程控制邏輯沒有那麼明顯,也就沒有那麼必要使用狀態機,引入不必要的複雜性,同理,如果希望流程更加靈活,可以通過非編程的方式動態組織這些流程,那麼可能流程引擎更適合,而不是狀態機。
狀態機爲什麼這麼小衆,還是因爲使用的場景過於狹窄。
02 實現一個狀態機
其實實現一個狀態機很簡單,可以像下面這樣,一個類就能完成。
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
class StateMachine<S, E> {
private S currentState;
private final Map<S, Map<E, S>> transitions = new HashMap<>();
private final Map<S, Consumer<E>> entryActions = new HashMap<>();
private final Map<S, Consumer<E>> exitActions = new HashMap<>();
public StateMachine(S initialState) {
this.currentState = initialState;
}
public void addTransition(S fromState, S toState, E event) {
transitions.computeIfAbsent(fromState, k -> new HashMap<>()).put(event, toState);
}
public void setEntryAction(S state, Consumer<E> action) {
entryActions.put(state, action);
}
public void setExitAction(S state, Consumer<E> action) {
exitActions.put(state, action);
}
public boolean handleEvent(E event) {
Map<E, S> stateTransitions = transitions.get(currentState);
if (stateTransitions != null) {
S nextState = stateTransitions.get(event);
if (nextState != null) {
// Perform exit action for the current state
if (exitActions.containsKey(currentState)) {
exitActions.get(currentState).accept(event);
}
// Transition to the next state
currentState = nextState;
// Perform entry action for the new state
if (entryActions.containsKey(currentState)) {
entryActions.get(currentState).accept(event);
}
return true;
}
}
return false;
}
public S getCurrentState() {
return currentState;
}
public Set<E> getAvailableEvents() {
Map<E, S> stateTransitions = transitions.get(currentState);
return stateTransitions != null ? stateTransitions.keySet() : Set.of();
}
}
這裏面有幾個概念:
-
狀態轉換:或者叫狀態流,需要定義那兩個狀態之間的轉換是被允許的。
-
事件:能夠觸發的事件信息。
-
事件處理器:定義進入狀態和退出狀態時的鉤子函數。
下面是一個使用的例子:
public class SimpleStateMachineExample {
enum OrderState {
NEW, PROCESSING, SHIPPED, DELIVERED, CANCELLED
}
enum OrderEvent {
PROCESS_ORDER, SHIP_ORDER, DELIVER_ORDER, CANCEL_ORDER, RETURN_ORDER
}
public static void main(String[] args) {
StateMachine<OrderState, OrderEvent> orderStateMachine = new StateMachine<>(OrderState.NEW);
// 定義狀態轉換
orderStateMachine.addTransition(OrderState.NEW, OrderState.PROCESSING, OrderEvent.PROCESS_ORDER);
orderStateMachine.addTransition(OrderState.PROCESSING, OrderState.SHIPPED, OrderEvent.SHIP_ORDER);
orderStateMachine.addTransition(OrderState.SHIPPED, OrderState.DELIVERED, OrderEvent.DELIVER_ORDER);
orderStateMachine.addTransition(OrderState.NEW, OrderState.CANCELLED, OrderEvent.CANCEL_ORDER);
orderStateMachine.addTransition(OrderState.PROCESSING, OrderState.CANCELLED, OrderEvent.CANCEL_ORDER);
orderStateMachine.addTransition(OrderState.SHIPPED, OrderState.CANCELLED, OrderEvent.RETURN_ORDER);
// 定義狀態處理器
orderStateMachine.setEntryAction(OrderState.PROCESSING, event -> System.out.println("Order is being processed."));
orderStateMachine.setExitAction(OrderState.PROCESSING, event -> System.out.println("Exiting processing state."));
// 模擬觸發一個事件
simulateEvent(orderStateMachine, OrderEvent.PROCESS_ORDER);
simulateEvent(orderStateMachine, OrderEvent.SHIP_ORDER);
simulateEvent(orderStateMachine, OrderEvent.DELIVER_ORDER);
System.out.println("Final State: " + orderStateMachine.getCurrentState());
}
private static void simulateEvent(StateMachine<OrderState, OrderEvent> stateMachine, OrderEvent event) {
System.out.println("Current State: " + stateMachine.getCurrentState());
System.out.println("Handling Event: " + event);
if (!stateMachine.handleEvent(event)) {
System.out.println("Event " + event + " cannot be handled from state " + stateMachine.getCurrentState());
}
System.out.println("New State: " + stateMachine.getCurrentState());
System.out.println("------------------------");
}
}
也可以升級一下狀態機,把定義狀態轉換、定義狀態處理器這些邏輯定義成 DSL,放到配置文件中,也可以可視化狀態轉換的過程、審計日誌。
這就是把狀態邏輯和業務邏輯分離的價值。
上面這個狀態機太簡陋了,但是大多數場景下也夠用,想實現我們提到的更多功能,用於調試、管理狀態變化,可以用一些更加專業的庫來實現。
03 使用 Spring Statemachine 項目
Spring Statemachine 就是一個基於 Spring 生態的狀態機庫。 那麼如果是 Spring 這個框架來做的話,有什麼更多特性呢?
支持層級狀態
Spring Statemachine 支持層級(嵌套)狀態,這意味着一個狀態機可以包含另一個狀態機。這對於需要處理複雜邏輯的應用程序特別有用,可以將狀態分層,減少複雜性。
支持併發狀態
前面的狀態機非常致命的一個地方就是併發,如果狀態機是一個單例類就會出現併發問題,如果爲每一次請求創建一個狀態機實例可以避免併發問題,但是這樣會頻繁創建和銷燬對象。
如果上鎖,系統串行化更不划算。
Spring Statemachine 基於一些併發編程的模式,優化了這裏,所以更加高效。
其它特性
這些特性一提大家都明白,就不過多展開了。
-
圖形化狀態圖生成
-
動態添加和移除狀態
-
支持條件狀態轉換,這個是指除了固定的狀態轉換外,可以帶上條件。例如,報銷審批單總價小於 100 就不用審批。
-
異步事件處理
04 Spring Statemachine 使用示例
Spring Statemachine 支持使用 YAML 來定義規則,這樣定義狀態就非常直觀。
statemachine:
states:
- READY
- PROCESSING
- COMPLETED
- ERROR
transitions:
- from: READY
to: PROCESSING
event: START_PROCESS
- from: PROCESSING
to: COMPLETED
event: COMPLETE_PROCESS
- from: PROCESSING
to: ERROR
event: ERROR_OCCURRED
@Configuration
public class MyStateMachineConfig extends StateMachineConfigurerAdapter<String, String> {
@Value("${statemachine.states}")
private List<String> states;
@Value("#{${statemachine.transitions}}")
private List<Map<String, String>> transitions;
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
// 配置初始狀態和其他狀態
states.withStates()
.initial(this.states.get(0)) // 假設第一個狀態爲初始狀態
.states(new HashSet<>(this.states)); // 添加所有狀態
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
for (Map<String, String> transition : this.transitions) {
transitions.withExternal()
.source(transition.get("from"))
.target(transition.get("to"))
.event(transition.get("event"));
}
}
}
其實 Spring Statemachine 本身不直接支持 YAML 配置,但通過結合 Spring Boot 和 YAML 文件,我們可以方便地管理狀態機的配置。
根據我們這個系列前面關於如何處理業務規則的文章,把狀態納入業務規則也是一種非常有用的技巧,這樣業務人員就能很容易看到當前系統如何實現這些狀態了。
05 補充:有限狀態機和無限狀態機
有限狀態機 FSM(finite-state machine) 是一種具有有限個狀態的抽象機器,它可以從一個狀態轉換到另一個狀態。FSM 可以通過輸入的事件或條件來改變其狀態。FSM 通常用作控制系統和計算機科學中的邏輯電路、協議分析等。
由於很多場景下,有限狀態機模型和業務非常匹配,所以是一個非常好的設計模式。其實我們寫在業務代碼中,也在使用這個模型,前面的 Order 類中每個方法就是事件處理函數,只不過我們並沒有把業務邏輯和狀態管理邏輯分離而已。
那麼爲什麼叫有限呢?因爲還有無限狀態機(Infinite State Machine)。無限狀態機擁有無限個狀態,通常適用於狀態數量是動態的或不可預見的情況。
有限狀態機和無限狀態機是兩種描述系統行爲的方法。爲了更好地理解它們之間的區別,可以將它們比作現實生活中的不同場景:有限狀態機常用於模型中需要管理固定數量的狀態的問題,如業務單據流轉、簡單的遊戲關卡、協議解析等。
無限狀態機可以比作一個計數器,像一個水錶或電錶。每次用水或電(輸入),計數器的讀數(狀態)都會增加,理論上這個數可以無限增長。無限狀態機適用於那些狀態不是事先固定或可以無限變化的問題,如動態內存管理、實時數據流處理、複雜軟件系統的調度。
參考資料
-
https://spring.io/projects/spring-statemachine
-
項目終於用上了 Spring 狀態機,非常優雅! https://zhuanlan.zhihu.com/p/632003000
-
Spring 狀態機的介紹與使用 https://www.cnblogs.com/yunjie0930/p/17954492
-
How to implement a FSM - Finite State Machine in Java https://stackoverflow.com/questions/13221168/how-to-implement-a-fsm-finite-state-machine-in-java
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/jN7Pv5Hrpb0ZWZ4I_GkvZg