還在用ifelse來寫業務?瞭解下Spring狀態機

来源:https://www.cnblogs.com/r0ad/p/18075514
-Advertisement-
Play Games

當開發者意識到代碼庫開始變得般混亂不堪時,就會在現有項目中引入狀態機。狀態機的引入有助於將複雜多變的應用程式狀態轉換過程組織得更為有序和清晰,從而避免代碼陷入難以維護的境地。 ...


狀態機之所以強大,是因為其行為在啟動時就以固定的方式定義了操作規則,從而確保了一貫的連貫性和相對較高的可調試性。關鍵在於,應用程式處於且僅可能處於有限數量的狀態中。然後,某些事件發生會使得應用從一個狀態過渡到另一個狀態。狀態機由觸發器驅動,這些觸發器基於事件或計時器。

設計高層次邏輯並將其置於應用程式外部,然後通過多種方式與狀態機交互,這種方式要簡單得多。可以通過發送事件、監聽狀態機的行為或請求當前狀態來與狀態機進行交互。

當開發者意識到代碼庫開始變得般混亂不堪時,就會在現有項目中引入狀態機。麵條代碼表現為無盡的、層級化的IF、ELSE和BREAK子句結構,當事情變得過於複雜時,編譯器或許應該建議開發者暫停一下,先休息一下。狀態機的引入有助於將複雜多變的應用程式狀態轉換過程組織得更為有序和清晰,從而避免代碼陷入難以維護的境地。

什麼是狀態

狀態是狀態機可能處於的一種模型。相比於在通用文檔中使用抽象概念,通過現實生活中的例子來描述狀態通常更為直觀易懂。以一個簡單的鍵盤為例——我們大多數人每天都使用它。如果你有一個標準鍵盤,左側有普通鍵,右側有數字小鍵盤,你可能會註意到,根據Numlock(數字鎖定)是否激活,數字小鍵盤可以處於兩種不同的狀態。如果沒有激活,按下數字小鍵盤的按鍵會實現方嚮導航等功能;如果數字小鍵盤被激活,則按下這些鍵將輸入數字。本質上,鍵盤的數字小鍵盤部分可以處於兩種不同的狀態。

將狀態的概念聯繫到編程上,這意味著我們可以不再依賴於標誌位、嵌套的if/else/break語句或其他不切實際(有時甚至是曲折複雜的)邏輯,而是可以通過狀態、狀態變數或與狀態機的交互來處理問題。換句話說,在編程中運用狀態這一概念,能夠幫助我們更清晰地組織和管理程式的不同狀態及其轉換過程。

什麼是狀態機

狀態機是一種理論模型,它描述了一個對象在其生命周期內可能經歷的有限數量的狀態及其之間的轉換規則。每個狀態都有觸髮狀態遷移的條件(通常是事件),並且可以關聯執行的動作。

狀態機的核心在於狀態變遷和事件驅動,適合處理非同步和併發的情況。狀態機強調的是系統當前所處的狀態,並且關註於系統如何根據接收到的外部事件或內部條件進行狀態轉變。

狀態機最常見於嵌入式系統、用戶界面交互設計、游戲開發、網路協議解析等領域。

以下以游戲馬利奧的狀態切換為例,來理解狀態機的使用場景:

graph LR A[小馬利奧] -->|吃蘑菇| B[超級馬利奧] B -->|吃花| C[火焰馬利奧] C -->|被敵人碰到| B B -->|被敵人碰到| A

與狀態設計模式的區別

在面向對象編程中,狀態設計模式是一種行為型設計模式,允許對象在其內部狀態改變時改變其行為。該模式通過將每一個狀態封裝成一個類,使得當對象的狀態發生改變時,它的行為也隨之改變,同時能夠使代碼更加清晰和模塊化。

在狀態設計模式中,每個狀態是一個單獨的類實例,這些類通常會實現一個公共介面,以便上下文對象可以調用適當的方法,而無需知道具體當前處於哪種狀態。上下文對象(context)持有對當前狀態對象的引用,併在接收到特定事件時調用狀態對象的方法來處理事件並可能導致狀態切換。

聯繫

  • 狀態設計模式是對狀態機理論的一種實現,它把狀態機的概念應用於軟體設計中,利用面向對象技術實現了狀態的抽象、封裝和擴展性。

區別

  • 狀態機是一個抽象的概念,可以不依賴於任何特定的編程語言或設計模式獨立存在。
  • 狀態設計模式則是具體的編程實踐,是針對解決狀態轉換問題的一種設計解決方案,特別適用於面向對象環境下的複雜狀態管理。

與流程引擎的區別

流程引擎(Business Process Management Engine, BPMN Engine)是實現業務流程管理(BPM)的軟體組件,主要用於執行和監控預定義的工作流程。這些工作流程通常包括一系列順序執行的任務或活動,具有明確的開始點、結束點和中間過程。

流程引擎支持更複雜的流程結構,如並行分支、同步合併、迴圈等,並提供了豐富的建模語言(如BPMN)來可視化表示流程邏輯。流程引擎不僅關註狀態轉移,還註重任務分配、資源調度、事務處理以及流程實例的整體生命周期管理。

流程引擎適用於企業級應用中需要自動化、規範化和優化的複雜業務流程,比如採購審批流程、貸款審批流程、訂單處理流程等。

區別與聯繫:

  • 目的性不同: 狀態機主要解決狀態變化的問題,而流程引擎則更多地關註流程的整體組織和執行。
  • 結構靈活性: 狀態機結構相對簡單,特別適合清晰、固定的流程;流程引擎支持多層次、多路徑的複雜流程,允許動態調整和擴展。
  • 參與角色: 狀態機側重於機器層面的自動化處理,流程引擎則常涉及人的參與決策和協同工作。
  • 集成度: 流程引擎通常包含更多的管理和監控功能,能夠與組織其他系統緊密集成,提供強大的審計跟蹤、異常處理和數據分析能力。
  • 聯繫: 在實際項目中,狀態機的概念和機制可能會作為流程引擎的一部分被採用,尤其是在流程中有明顯的狀態變遷環節時。同時,兩者都可以用作業務規則和流程規範的有效工具,只不過各自聚焦的領域和複雜程度有所差異。

什麼是Spring狀態機

Spring Statemachine(SSM)是一個框架,允許應用程式開發者在Spring應用中使用傳統的狀態機概念。SSM提供了以下功能:

  1. 為簡單用例提供易於使用的單層(一級)狀態機。
  2. 採用分層狀態機結構,便於配置複雜狀態。
  3. 狀態機區域以支持更為複雜的狀態配置。
  4. 支持觸發器、轉換、守衛和動作的使用。
  5. 提供類型安全的配置適配器。
  6. 集成了狀態機事件監聽器。
  7. 與Spring IoC(控制反轉)集成,可將Bean關聯至狀態機。

SSM有哪些使用場景

項目適於使用狀態機的場景包括:

  1. 當你可以將應用程式或其部分結構表示為一系列狀態時,該項目是應用狀態機的良好候選者。
  2. 你希望將複雜的邏輯拆分為更小、更易於管理的任務。
  3. 應用程式已經存在併發問題,例如非同步操作導致的問題。

在以下情況下,實際上你已經在嘗試實現一個狀態機:

  1. 使用布爾標誌或枚舉來模擬各種情況。這意味著你的代碼可能在通過這些標誌和枚舉跟蹤不同狀態。
  2. 擁有僅在應用程式生命周期中的某些階段才有意義的變數。這暗示了狀態變化對程式流程的影響。
  3. 正在迴圈遍歷if-else結構(或者更糟糕的是,多個這樣的結構),檢查特定標誌或枚舉是否已設置,然後根據這些標誌和枚舉是否存在及其組合進一步判斷接下來的操作。這種編程方式本質上是在手動處理狀態轉移,而採用狀態機可以更清晰、規範地表述並簡化此類複雜的狀態轉換邏輯。

如何集成SSM

需要在maven或者gradle中ssm的依賴。

<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-starter</artifactId>
    <version>4.0.0</version>
</dependency>
implementation 'org.springframework.statemachine:spring-statemachine-starter:4.0.0'

如何使用SSM

下麵以一個簡單的例子來說明如何使用SSM。

// 定義對應狀態和事件的枚舉:
public enum States {
    SI, S1, S2
}

public enum Events {
    E1, E2
}

// 定義狀態機的配置
import java.util.EnumSet;

@Configuration // 標識為配置類
@EnableStateMachine // 啟用狀態機功能
public class StateMachineConfig
        extends EnumStateMachineConfigurerAdapter<States, Events> {

    /**
     * 配置狀態機的全局屬性,如自動啟動和狀態監聽器。
     *
     * @param config 狀態機配置構建器
     * @throws Exception 如果配置過程中發生錯誤
     */
    @Override
    public void configure(StateMachineConfigurationConfigurer<States, Events> config)
            throws Exception {
        config
                .withConfiguration()
                .autoStartup(true) // 設置狀態機自動啟動
                .listener(listener()); // 註冊狀態改變監聽器
    }

    /**
     * 配置狀態機的狀態。
     *
     * @param states 狀態配置構建器
     * @throws Exception 如果配置過程中發生錯誤
     */
    @Override
    public void configure(StateMachineStateConfigurer<States, Events> states)
            throws Exception {
        states
                .withStates()
                .initial(States.SI) // 設置初始狀態為SI
                .states(EnumSet.allOf(States.class)); // 將所有枚舉狀態添加到狀態機
    }

    /**
     * 配置狀態機的轉換。
     *
     * @param transitions 轉換配置構建器
     * @throws Exception 如果配置過程中發生錯誤
     */
    @Override
    public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
            throws Exception {
        transitions
                .withExternal() // 配置外部觸發的轉換
                .source(States.SI).target(States.S1).event(Events.E1) // 定義從SI到S1的轉換,由事件E1觸發
                .and() // 連接另一個轉換配置
                .withExternal() // 另一個外部觸發的轉換
                .source(States.S1).target(States.S2).event(Events.E2); // 定義從S1到S2的轉換,由事件E2觸發
    }

    /**
     * 創建並返回一個狀態機監聽器,用於監聽狀態的改變。
     *
     * @return 狀態機監聽器實例
     */
    @Bean
    public StateMachineListener<States, Events> listener() {
        return new StateMachineListenerAdapter<States, Events>() {
            @Override
            public void stateChanged(State<States, Events> from, State<States, Events> to) {
                if(from != null){
                    System.out.println("State change from " + from.getId());
                }
                System.out.println("State change to " + to.getId());
            }
        };
    }
}

測試代碼如下:

@RestController
@Tag(name = "狀態機", description = "狀態機")
public class StateController{
    @Autowired
    private StateService stateService;

    @GetMapping(value = "改變狀態")
    @Operation(description = "改變狀態")
    public void change() {
        stateService.changeState();
    }
}
/**
 * 狀態機演示服務
 */
@Service
public class StateService {
    @Autowired
    private StateMachine<States, Events> stateMachine;

    public void changeState() {
        stateMachine.sendEvent(Events.E1);
        stateMachine.sendEvent(Events.E2);
    }
}

服務層的輸出的結果如下:

State change to SI
State change from SI
State change to S1
State change from S1
State change to S2

以上代碼只是簡單演示了SSM的集成和使用demo。實際業務場景可能更為複雜,需要根據實際需求進行擴展。

除了狀態,要更好的使用SSM還需要理解偽狀態等很多概念,比如Junction(允許多個傳入轉換)、 Fork(一個或多個區域的顯式入口)、Join (將源自不同區域的多個過渡合併在一起)。這部分內容將在後續文章中進行介紹。

參考

關於作者

來自一線全棧程式員nine的八年探索與實踐,持續迭代中。歡迎關註公眾號“雨林尋北”或添加個人衛星codetrend(備註技術)。


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

-Advertisement-
Play Games
更多相關文章
  • 前言 如果趕時間請直接使用目錄跳到解決問題的部分。 使用的項目使用vue腳手架生成。 npm init vue@latest 版本如下 "@vitejs/plugin-vue": "^5.0.4", "vue": "^3.4.21" 由於近期在學less,心想如果不能將其應用到vue項目中,無異於紙 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 一、介紹 Promise,譯為承諾,是非同步編程的一種解決方案,比傳統的解決方案(回調函數)更加合理和更加強大 在以往我們如果處理多層非同步操作,我們往往會像下麵那樣編寫我們的代碼 doSomething(function(result) { ...
  • C語言中抽象函數與具體實現的命名與組織 在C語言的項目開發中,尤其是嵌入式系統和開源軟體項目里,合理地命名和組織抽象函數及其具體實現對於提高代碼的可讀性、可維護性和可擴展性至關重要。以下是關於如何在這些項目中有效地處理抽象和實現的一些建議: 抽象函數與具體實現的區分 API作為介面:API定義了一組 ...
  • Java 線程 線程使程式能夠通過同時執行多個任務而更有效地運行。 線程可用於在不中斷主程式的情況下在後臺執行複雜的任務。 創建線程 有兩種創建線程的方式。 擴展Thread類 可以通過擴展Thread類並覆蓋其run()方法來創建線程: public class MyThread extends ...
  • OI 一場空,不開 long long 見祖宗 cmp,一定要在 sort 里寫入 打 st 表一定要算空間複雜度 打倍增 LCA 一定要算空間複雜度 註意 ÷0 線段樹 4 倍空間 無向圖,鏈式前向星 2 倍空間 樹鏈剖分要註意是原編號還是 dfn 序的編號 鏈式前向星遍歷圖的時間複雜度永遠為 + ...
  • 一、數據類型 定義: 就是用了保存數據的一個類型,一種數據類型,只能保存該類型數據值 作用: 只有瞭解數據類型,才能選擇合適的類型存放數據,才能更好的利用電腦硬體資源(記憶體和硬碟等)。 不同的數據類型存放數據大小是不同的。 數據類型的使用方式就是用來聲明一個變數,裝數據的。 常用的整數類型是 in ...
  • 在開發過程中,碰到需要在 Python Django 項目中連接到位於 ECS 上但未開通外網地址的 RDS 資料庫。 這種情況下,通過建立 SSH 隧道來實現連接,確保數據傳輸的安全性和可靠性。 1、安裝 sshtunnel 包 pip3 install sshtunnel 2、導入 SSHTun ...
  • 在MyBatis中,如果你使用resultType而不是resultMap,並且結果集中有同名欄位,則預設情況下後出現的欄位值會覆蓋前面的欄位值。這是因為MyBatis在將結果集映射到Java對象時,是按照欄位名稱一一對應進行賦值的。 但若你希望更精確地控制映射關係,並且避免自動覆蓋行為,則可以用r ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...