狀態機的技術選型,yyds!

来源:https://www.cnblogs.com/StarbucksBoy/archive/2022/11/14/16888787.html
-Advertisement-
Play Games

今天跟大家分享一個關於“狀態機”的話題。給你講清楚什麼是狀態機、為什麼需要狀態機、適用場景、有哪些具體的實現方案以及各個方案對比(附帶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 種設計模式之一,屬於行為模式的一種。

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

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

  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資料。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 模板 c++另一種編程思想稱為泛型編程,主要利用的技術就是模板 c++提供兩種模板機制:函數模板和類模板 函數模板 建立一個通用函數,函數的返回值類型和形參類型可以不具體指定,用一個虛擬的類型來代表 語法: template<typename T> //或者 template<class T> 函數 ...
  • 渲染模板 我的客服系統後端使用的golang Gin 框架,想把頁面渲染出來,下麵就是載入html模板頁面 package router func InitViewRouter(engine *gin.Engine) { //關於頁面 engine.GET("/aboutus.html", func ...
  • 在看集合源碼的時候,因為對一些知識點有些混淆,導致看源碼比較吃力。所以重新回顧一下麵向對象的繼承和多態,順便記錄一下重點。 繼承 子類會繼承父類的所有屬性和方法,但私有屬性和方法在子類不能直接訪問,需要通過父類提供的公共方法訪問; 子類必須調用父類的構造器,完成父類的初始化(創建子類對象時會調用父類 ...
  • 本文花了較短的篇幅重點介紹了JVM Sandbox的功能,實際用法,以及基礎原理。它通過封裝一些底層JVM控制的框架,使得對JVM層面的AOP開發變的異常簡單,就像作者自己所說“JVM-SANDBOX還能幫助你做很多很多,取決於你的腦洞有多大了。” ...
  • 本篇學習 Yarn Application 編寫方法,將帶你更清楚的瞭解一個任務是如何提交到 Yarn ,在運行中的交互和任務停止的過程。通過瞭解整個任務的運行流程,幫你更好的理解 Yarn 運作方式,出現問題時能更好的定位。 一、簡介 本篇將對 Yarn Application 編寫流程進行介紹。 ...
  • 數據結構是Python中一個很重要的概念,是以某種方式(如通過編號)組合起來的數據元素(如數字、字元乃至其他數據結構)的集合。 在Python中,最基本的數據結構是序列(sequence)。 序列中的每個元素都有編號,及其位置或索引,其中的第一個元素的索引為0,第二個元素位的索引為1,依此類推 在有 ...
  • 先說結論 : extern "C"隻影響到鏈接期的name mangling 什麼是name mangling? 請看 : C++函數重載的實現機制之name mangling - 知乎 (zhihu.com) 舉個例子 : // external.h #ifdef __cplusplus exte ...
  • 迷人的兩度搜索 1、BFS和DFS 深度優先搜索演算法(DFS)和廣度優先搜索演算法(BFS)是一種用於遍歷或搜索樹或圖的演算法,在搜索遍歷的過程中保證每個節點(頂點)訪問一次且僅訪問一次,按照節點(頂點)訪問順序的不同分為深度優先和廣度優先。 1.1、深度優先搜索演算法 深度優先搜索演算法(Depth-Fi ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...