狀態機之所以強大,是因為其行為在啟動時就以固定的方式定義了操作規則,從而確保了一貫的連貫性和相對較高的可除錯性。關鍵在於,應用程式處於且僅可能處於有限數量的狀態中。然後,某些事件發生會使得應用從一個狀態過渡到另一個狀態。狀態機由觸發器驅動,這些觸發器基於事件或計時器。
設計高層次邏輯並將其置於應用程式外部,然後透過多種方式與狀態機互動,這種方式要簡單得多。可以透過傳送事件、監聽狀態機的行為或請求當前狀態來與狀態機進行互動。
當開發者意識到程式碼庫開始變得般混亂不堪時,就會在現有專案中引入狀態機。麵條程式碼表現為無盡的、層級化的IF、ELSE和BREAK子句結構,當事情變得過於複雜時,編譯器或許應該建議開發者暫停一下,先休息一下。狀態機的引入有助於將複雜多變的應用程式狀態轉換過程組織得更為有序和清晰,從而避免程式碼陷入難以維護的境地。
什麼是狀態
狀態是狀態機可能處於的一種模型。相比於在通用文件中使用抽象概念,透過現實生活中的例子來描述狀態通常更為直觀易懂。以一個簡單的鍵盤為例——我們大多數人每天都使用它。如果你有一個標準鍵盤,左側有普通鍵,右側有數字小鍵盤,你可能會注意到,根據Numlock(數字鎖定)是否啟用,數字小鍵盤可以處於兩種不同的狀態。如果沒有啟用,按下數字小鍵盤的按鍵會實現方向導航等功能;如果數字小鍵盤被啟用,則按下這些鍵將輸入數字。本質上,鍵盤的數字小鍵盤部分可以處於兩種不同的狀態。
將狀態的概念聯絡到程式設計上,這意味著我們可以不再依賴於標誌位、巢狀的if/else/break語句或其他不切實際(有時甚至是曲折複雜的)邏輯,而是可以透過狀態、狀態變數或與狀態機的互動來處理問題。換句話說,在程式設計中運用狀態這一概念,能夠幫助我們更清晰地組織和管理程式的不同狀態及其轉換過程。
什麼是狀態機
狀態機是一種理論模型,它描述了一個物件在其生命週期內可能經歷的有限數量的狀態及其之間的轉換規則。每個狀態都有觸發狀態遷移的條件(通常是事件),並且可以關聯執行的動作。
狀態機的核心在於狀態變遷和事件驅動,適合處理非同步和併發的情況。狀態機強調的是系統當前所處的狀態,並且關注於系統如何根據接收到的外部事件或內部條件進行狀態轉變。
狀態機最常見於嵌入式系統、使用者介面互動設計、遊戲開發、網路協議解析等領域。
以下以遊戲馬里奧的狀態切換為例,來理解狀態機的使用場景:
與狀態設計模式的區別
在物件導向程式設計中,狀態設計模式是一種行為型設計模式,允許物件在其內部狀態改變時改變其行為。該模式透過將每一個狀態封裝成一個類,使得當物件的狀態發生改變時,它的行為也隨之改變,同時能夠使程式碼更加清晰和模組化。
在狀態設計模式中,每個狀態是一個單獨的類例項,這些類通常會實現一個公共介面,以便上下文物件可以呼叫適當的方法,而無需知道具體當前處於哪種狀態。上下文物件(context)持有對當前狀態物件的引用,並在接收到特定事件時呼叫狀態物件的方法來處理事件並可能導致狀態切換。
聯絡:
- 狀態設計模式是對狀態機理論的一種實現,它把狀態機的概念應用於軟體設計中,利用物件導向技術實現了狀態的抽象、封裝和擴充套件性。
區別:
- 狀態機是一個抽象的概念,可以不依賴於任何特定的程式語言或設計模式獨立存在。
- 狀態設計模式則是具體的程式設計實踐,是針對解決狀態轉換問題的一種設計解決方案,特別適用於物件導向環境下的複雜狀態管理。
與流程引擎的區別
流程引擎(Business Process Management Engine, BPMN Engine)是實現業務流程管理(BPM)的軟體元件,主要用於執行和監控預定義的工作流程。這些工作流程通常包括一系列順序執行的任務或活動,具有明確的開始點、結束點和中間過程。
流程引擎支援更復雜的流程結構,如並行分支、同步合併、迴圈等,並提供了豐富的建模語言(如BPMN)來視覺化表示流程邏輯。流程引擎不僅關注狀態轉移,還注重任務分配、資源排程、事務處理以及流程例項的整體生命週期管理。
流程引擎適用於企業級應用中需要自動化、規範化和最佳化的複雜業務流程,比如採購審批流程、貸款審批流程、訂單處理流程等。
區別與聯絡:
- 目的性不同: 狀態機主要解決狀態變化的問題,而流程引擎則更多地關注流程的整體組織和執行。
- 結構靈活性: 狀態機結構相對簡單,特別適合清晰、固定的流程;流程引擎支援多層次、多路徑的複雜流程,允許動態調整和擴充套件。
- 參與角色: 狀態機側重於機器層面的自動化處理,流程引擎則常涉及人的參與決策和協同工作。
- 整合度: 流程引擎通常包含更多的管理和監控功能,能夠與組織其他系統緊密整合,提供強大的審計跟蹤、異常處理和資料分析能力。
- 聯絡: 在實際專案中,狀態機的概念和機制可能會作為流程引擎的一部分被採用,尤其是在流程中有明顯的狀態變遷環節時。同時,兩者都可以用作業務規則和流程規範的有效工具,只不過各自聚焦的領域和複雜程度有所差異。
什麼是Spring狀態機
Spring Statemachine(SSM)是一個框架,允許應用程式開發者在Spring應用中使用傳統的狀態機概念。SSM提供了以下功能:
- 為簡單用例提供易於使用的單層(一級)狀態機。
- 採用分層狀態機結構,便於配置複雜狀態。
- 狀態機區域以支援更為複雜的狀態配置。
- 支援觸發器、轉換、守衛和動作的使用。
- 提供型別安全的配置介面卡。
- 整合了狀態機事件監聽器。
- 與Spring IoC(控制反轉)整合,可將Bean關聯至狀態機。
SSM有哪些使用場景
專案適於使用狀態機的場景包括:
- 當你可以將應用程式或其部分結構表示為一系列狀態時,該專案是應用狀態機的良好候選者。
- 你希望將複雜的邏輯拆分為更小、更易於管理的任務。
- 應用程式已經存在併發問題,例如非同步操作導致的問題。
在以下情況下,實際上你已經在嘗試實現一個狀態機:
- 使用布林標誌或列舉來模擬各種情況。這意味著你的程式碼可能在透過這些標誌和列舉跟蹤不同狀態。
- 擁有僅在應用程式生命週期中的某些階段才有意義的變數。這暗示了狀態變化對程式流程的影響。
- 正在迴圈遍歷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 (將源自不同區域的多個過渡合併在一起)。這部分內容將在後續文章中進行介紹。
參考
- A State Machine Crash Course - spring-statemachine https://docs.spring.io/spring-statemachine/docs/4.0.0/reference/index.html#crashcourse
關於作者
來自一線全棧程式設計師nine的八年探索與實踐,持續迭代中。歡迎關注公眾號“雨林尋北”或新增個人衛星codetrend(備註技術)。