狀態機的技術選型,yyds!

2022-11-14 15:00:40

前言

今天跟大家分享一個關於「狀態機」的話題。狀態屬性在我們的現實生活中無處不在。比如電商場景會有一系列的訂單狀態(待支付、待發貨、已發貨、超時、關閉);員工提交請假申請會有申請狀態(已申請、稽核中、稽核成功、稽核拒絕、結束);差旅報銷單會有單據稽核狀態(已提交、稽核中、稽核成功、退回、打款中、打款成功、打款失敗、結束)等等。上述場景有一個共同問題:根據不同觸發條件執行不同處理動作最後落地不同的狀態。範例程式碼如下:

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 種設計模式之一,屬於行為模式的一種。

它允許一個物件在其內部狀態改變時改變它的行為。物件看起來似乎修改了它的類。

作用狀態模式的設計意圖主要是為了解決兩個主要問題:

  1. 當一個物件的內部狀態改變時,它應該改變它的行為。

  2. 應獨立定義特定於狀態的行為。也就是說,新增新狀態不應影響現有狀態的行為。

類圖:

類圖

定義一個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地址:

github.com/TaoZhuGongB…

總結

好了,此篇文章即將進入尾聲,讓我們一起來做個總結。

為什麼引入狀態機?

前言部分我也提到了在面對複雜的狀態流轉場景下if-else方案主要容易引起可讀性、可延伸、易出錯等問題,所以引入狀態機主要為了降低這些風險。

狀態機的實現方案對比:

狀態機實現方案我舉例了Java列舉、狀態模式、開源狀態機等幾個實現方案。狀態模式的問題是它需要定義介面、和實現類還附帶一個Context上下文類,編碼層面比較複雜。Java列舉版的狀態機主要問題是擴充套件粒度不夠基本都是線性擴充套件,封裝在一個類中,太複雜的狀態流轉這個類也會變得臃腫不堪,維護性變低。
所以也推薦了一款比較理想的開源狀態機實現--cola-component-statemachine。它使用相當簡單,因為實現了內部DSL,所以可讀性很強,當然擴充套件性也比較不錯。

公眾號:

裡面不僅彙集了硬核的乾貨技術、還彙集了像左耳朵耗子、張朝陽總結的高效學習方法論、職場升遷竅門、軟技能。希望能輔助你達到你想夢想之地!

公眾號內回覆關鍵字電子書」下載pdf格式的電子書籍(並行程式設計、JVM、MYSQL、JAVAEE、Linux、Spring、分散式等,你想要的都有!)、「開發手冊」獲取阿里開發手冊2本、"面試"獲取面試PDF資料。