系統設計 - 使用有限狀態機優化應用狀態管理

公衆號: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 基於一些併發編程的模式,優化了這裏,所以更加高效。

其它特性

這些特性一提大家都明白,就不過多展開了。

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)。無限狀態機擁有無限個狀態,通常適用於狀態數量是動態的或不可預見的情況。

有限狀態機和無限狀態機是兩種描述系統行爲的方法。爲了更好地理解它們之間的區別,可以將它們比作現實生活中的不同場景:有限狀態機常用於模型中需要管理固定數量的狀態的問題,如業務單據流轉、簡單的遊戲關卡、協議解析等。

無限狀態機可以比作一個計數器,像一個水錶或電錶。每次用水或電(輸入),計數器的讀數(狀態)都會增加,理論上這個數可以無限增長。無限狀態機適用於那些狀態不是事先固定或可以無限變化的問題,如動態內存管理、實時數據流處理、複雜軟件系統的調度。

參考資料

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/jN7Pv5Hrpb0ZWZ4I_GkvZg