Squirrel狀態機-從原理探究到最佳實踐

2023-01-28 18:00:19

作者:京東物流 鄭朋輝

1 簡介

Squirrel狀態機是一種用來進行物件行為建模的工具,主要描述物件在它的生命週期內所經歷的狀態,以及如何響應來自外界的各種事件。比如訂單的建立、已支付、發貨、收穫、取消等等狀態、狀態之間的控制、觸發事件的監聽,可以用該框架進行清晰的管理實現。使用狀態機來管理物件生命流的好處更多體現在程式碼的可維護性、可測試性上,明確的狀態條件、原子的響應動作、事件驅動遷移目標狀態,對於流程複雜易變的業務場景能大大減輕維護和測試的難度。

2 基本概念

2.1 Squirrel狀態機定義

Squirrel狀態機是一種有限狀態機,有限狀態機是指物件有一個明確並且複雜的生命流(一般而言三個以上狀態),並且在狀態變遷存在不同的觸發條件以及處理行為。

2.2 Squirrel狀態機要素

Squirrel狀態機可歸納為4個要素,即現態、條件、動作、次態。「現態」和「條件」是因,「動作」和「次態」是果。

  • 現態:是指當前所處的狀態。
  • 條件:又稱為事件。當一個條件被滿足,將會觸發一個動作,或者執行一次狀態的遷移。
  • 動作:條件滿足後執行的動作。動作執行完畢後,可以遷移到新的狀態,也可以仍舊保持原狀態。動作不是必需的,當條件滿足後,也可以不執行任何動作,直接遷移到新狀態。
  • 次態:條件滿足後要遷往的新狀態。「次態」是相對於「現態」而言的,「次態」一旦被啟用,就轉變成新的「現態」了。

3 實現原理

3.1 店鋪稽核CASE

舉例,京東線上開店需要經過稽核才能正式上線,店鋪狀態有待稽核、已駁回、已稽核,對應操作有提交稽核,稽核通過,稽核駁回動作。現在需要實現一個店鋪稽核流程的需求。

3.2 方案對比

3.2.1 常用if-else或switch-case實現(分支模式)

圖1.if-else/switch-case模式實現流程圖

3.2.2 狀態機實現

圖2.狀態機模式實現流程圖

3.2.3 對比

通過引入狀態機,可以去除大量if-else if-else或者switch-case分支結構,直接通過當前狀態和狀態驅動表查詢行為驅動表,找到具體行為執行操作,有利於程式碼的維護和擴充套件。

3.3 實現原理

圖3.狀態機建立流程圖

  • StateMachine: StateMachine範例由StateMachineBuilder建立不被共用,對於使用annotation方式(或fluent api)定義的StateMachine,StateMachine範例即根據此定義建立,相應的action也由本範例執行,與spring的整合最終要的就是講spring的bean範例注入給由builder建立的狀態機範例;
  • StateMachineBuilder: 本質上是由StateMachineBuilderFactory建立的動態代理。被代理的StateMachineBuilder預設實現為StateMachineBuilderImpl,內部描述了狀態機範例建立細節包括State、Event、Context型別資訊、constructor等,同時也包含了StateMachine的一些全域性共用資源包括StateConverter、EventConverter、MvelScriptManager等。StateMachineBuilder可被複用,使用中可被實現為singleton;
  • StateMachineBuilderFactory: 為StateMachineBuilder建立的動態代理範例;

4 實踐分享

4.1 環境依賴

<dependency>
<groupId>org.squirrelframework</groupId>
<artifactId>squirrel-foundation</artifactId>
<version>0.3.9</version>
</dependency>

4.2 狀態機元素定義:狀態、事件

// 店鋪稽核狀態
public Enum ShopInfoAuditStatusEnum{
audit(0,"待稽核"),
agree(1,"稽核通過"),
reject(2,"稽核駁回");
}
// 店鋪稽核事件
public Enum ShopInfoAuditEvent{
SUBMIT, // 提交
AGREE, // 同意
REJECT; // 駁回
}

4.3 構建StateMachineBuilder範例

/**
* StateMachineBuilder範例
*/
public class StateMachineEngine <T extends UntypedStateMachine, S, E, C> implements ApplicationContextAware{


private ApplicationContext applicationContext;


private static Map<String,UntypedStateMachineBuilder> builderMap = new HashMap<String,UntypedStateMachineBuilder>();

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}


@Transactional
public void fire(Class<T> machine, S state, E event, C context) {
StateMachineBuilder stateMachineBuilder = this.getStateMachineBuilder(machine);
StateMachine stateMachine = stateMachineBuilder.newStateMachine(state,applicationContext);
stateMachine.fire(event, context);
}


private StateMachineBuilder getStateMachineBuilder(Class<T> stateMachine){
UntypedStateMachineBuilder stateMachineBuilder = builderMap.get(stateMachine.getName());
if(stateMachineBuilder == null){
stateMachineBuilder = StateMachineBuilderFactory.create(stateMachine,ApplicationContext.class);
builderMap.put(stateMachine.getName(),stateMachineBuilder);
}
return stateMachineBuilder;

4.4 建立具體店鋪狀態稽核狀態機

/**
* 店鋪稽核狀態機
*/
@States({
@State(name = "audit"),
@State(name = "agree"),
@State(name = "reject")
})
@Transitions({
@Transit(from = "audit", to = "agree", on = "AGREE", callMethod = "agree"),
@Transit(from = "audit", to = "reject", on = "REJECT", callMethod = "reject"),
@Transit(from = "reject", to = "audit", on = "SUBMIT", callMethod = "submit"),
@Transit(from = "agree", to = "audit", on = "SUBMIT", callMethod = "submit"),
@Transit(from = "audit", to = "audit", on = "SUBMIT", callMethod = "submit"),
})
@StateMachineParameters(stateType=ShopInfoAuditStatusEnum.class, eventType=ShopInfoAuditEvent.class, contextType=ShopInfoAuditStatusUpdateParam.class)
public class ShopInfoAuditStateMachine extends AbstractUntypedStateMachine {


private ApplicationContext applicationContext;


public ShopInfoAuditStateMachine(){}


public ShopInfoAuditStateMachine(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}


// 稽核通過業務邏輯
public void agree(ShopInfoAuditStatusEnum fromState, ShopInfoAuditStatusEnum toState, ShopInfoAuditEvent event, ShopInfoAuditStatusUpdateParam param) {
this.agree(fromState,toState,event,param);
}

// 稽核駁回業務邏輯
public void reject(ShopInfoAuditStatusEnum fromState, ShopInfoAuditStatusEnum toState, ShopInfoAuditEvent event, ShopInfoAuditStatusUpdateParam param) {
this.reject(fromState,toState,event,param);
}

// 提交業務邏輯
public void submit(ShopInfoAuditStatusEnum fromState, ShopInfoAuditStatusEnum toState, ShopInfoAuditEvent event, ShopInfoAuditStatusUpdateParam param) {
this.submit(fromState,toState,event,param);
}

4.5 使用者端呼叫

// 呼叫端
main{
StateMachineEngine stateMachineEngine = applicationContext.getBean(StateMachineEngine.class);
// 稽核通過調case
stateMachineEngine.fire(ShopInfoAuditStateMachine.class,ShopInfoAuditStatusEnum.audit,ShopInfoAuditEvent.AGREE,param);
// 稽核駁回case
stateMachineEngine.fire(ShopInfoAuditStateMachine.class,ShopInfoAuditStatusEnum.audit,ShopInfoAuditEvent.REJECT,param);
}

5 總結

狀態機很好的幫我們處理了物件狀態的流轉、事件的監聽以及外界的各種事件的響應。從程式碼設計角度減少了大量if-else/switch-case邏輯判斷,提高了程式碼的可維護性、擴充套件性,方便管理和測試。