github上fork2.4k,star8.7k的這款狀態機,原來長這樣!

陶朱公Boy發表於2022-11-30
大家好,我是陶朱公Boy。

前言

上一篇文章《關於狀態機的技術選型,最後一個真心好》我跟大家聊了一下關於”狀態機“的話題。
於是就有小夥伴私信我,自己專案也考慮引入cola-statemachine這款狀態機,但網上資料實在太少,能不能系統的介紹一下如何使用這款工具。
讀者有需求,是必須要滿足的,誰叫github上fork2.4k,star8.7k的這款狀態機,原來長這樣!
 
 
也是剛好前段時間因工作需要徒手寫了一個簡易版的工作流引擎(需要滿足任意節點動態編排),裡面涉及比較複雜的工作流狀態流轉,之前的if-else方案,實在搞的一團亂麻,自從引入了這款元件,一下子就解放了生產力。

▲原來的狀態

github上fork2.4k,star8.7k的這款狀態機,原來長這樣!
 
上面還只是if-else實現版本中很小一部分程式碼,基本都是多個switch巢狀,最裡面的switch又涉及多個if-else判斷,可維護性和健壯性不言而喻。。
 

▲改造後的狀態(完整版)

StateMachineBuilder<ProcessStatusEnum, NodeTypeEnum, Context> builder = StateMachineBuilderFactory.create();
        builder.internalTransition().within(ProcessStatusEnum.INIT).on(NodeTypeEnum.HEAD).when(alwaysTrue()).perform(doNextProcessStatus());
        builder.externalTransition().from(ProcessStatusEnum.INIT).to(END).on(NodeTypeEnum.HEAD).when(checkNextNodeIfEndComponet()).perform(doNextProcessStatus());
        builder.externalTransition().from(ProcessStatusEnum.SOURCE_YUNYIN).to(ProcessStatusEnum.SUBMIT_APPLY_PASS).on(NodeTypeEnum.SUBMIT_APPLY_COMPONET).when(checkIfPass()).perform(doNextProcessStatus());
        builder.externalTransition().from(ProcessStatusEnum.SOURCE_YUNYIN).to(ProcessStatusEnum.SUBMIT_APPLY_NOT_PASS).on(NodeTypeEnum.SUBMIT_APPLY_COMPONET).when(checkIfNotPass()).perform(doNextProcessStatus());
        builder.externalTransition().from(ProcessStatusEnum.SOURCE_FK).to(ProcessStatusEnum.FK_PASS).on(NodeTypeEnum.FK_COMPONET).when(checkIfPass()).perform(doNextProcessStatus());
        builder.externalTransition().from(ProcessStatusEnum.SOURCE_FK).to(ProcessStatusEnum.FK_AUDIT_NOT_PASS).on(NodeTypeEnum.FK_COMPONET).when(checkIfNotPass()).perform(doNextProcessStatus());
        builder.externalTransition().from(ProcessStatusEnum.SOURCE_FK).to(ProcessStatusEnum.FK_REFUSE).on(NodeTypeEnum.FK_COMPONET).when(checkIfRefuse()).perform(doNextProcessStatus());
        builder.externalTransition().from(ProcessStatusEnum.SOURCE_CW).to(ProcessStatusEnum.CW_PASS).on(NodeTypeEnum.CW_COMPONET).when(checkIfPass()).perform(doNextProcessStatus());
        builder.externalTransition().from(ProcessStatusEnum.SOURCE_CW).to(ProcessStatusEnum.CW_NOT_PASS).on(NodeTypeEnum.CW_COMPONET).when(checkIfNotPass()).perform(doNextProcessStatus());
        builder.externalTransition().from(ProcessStatusEnum.SOURCE_CW).to(ProcessStatusEnum.CW_REFUSE).on(NodeTypeEnum.CW_COMPONET).when(checkIfRefuse()).perform(doNextProcessStatus());
        builder.externalTransition().from(ProcessStatusEnum.SOURCE_AUDIT_COMPLETE).to(ProcessStatusEnum.AUDIT_TERMINATE).on(NodeTypeEnum.AUDIT_TERMINATE).when(alwaysTrue()).perform(doNextProcessStatus());
        builder.externalTransition().from(SOURCE_OP_CHANGE_LICENSE).to(ProcessStatusEnum.UPDATE_LICENSE_SUCCESS).on(NodeTypeEnum.CHANGE_LICENSE_COMPONET).when(checkIfPass()).perform(doNextProcessStatus());
        builder.externalTransition().from(SOURCE_OP_CHANGE_LICENSE).to(ProcessStatusEnum.UPDATE_LICENSE_FAILURE).on(NodeTypeEnum.CHANGE_LICENSE_COMPONET).when(checkIfNotPass()).perform(doNextProcessStatus());
        builder.externalTransition().from(SOURCE_END).to(END).on(NodeTypeEnum.TAIL).when(checkCurrentNodeIfEndComponet()).perform(doNextProcessStatus());
        return builder.build("processStatusMachine");
 
這麼點程式碼基本能滿足複雜工作流狀態流轉,足見這款元件是解決狀態流轉的利器。
github地址:https://github.com/alibaba/COLA/tree/master/cola-components/cola-component-statemachine
目前在github上:Fork:2.4k;Star:8.8k
github上fork2.4k,star8.7k的這款狀態機,原來長這樣!
 
那接下來,廢話不多說,我們先實戰一把,先學會如何使用它。如果你想更加深入、全面的去了解元件的架構,可以看下架構設計部分章節。
 

快速開始

接下來,我以一個員工請假案例作為背景,手把手帶大家演示一下如何使用此元件。

第一步:專案中引入Maven依賴

<dependency> 
   <groupId>com.alibaba.cola</groupId>
   <artifactId>cola-component-statemachine</artifactId> 
   <version>4.3.1</version> 
</dependency>

第二步:初始化狀態機

@Configuration
public class StateMachineRegist {
    private final String STATE_MACHINE_ID="stateMachineId";
    /**
     * 構建狀態機例項
     */
    @Bean
    public StateMachine<ApplyStatusEnum, Event, LeaveContext> stateMachine() {

        StateMachineBuilder<ApplyStatusEnum, Event, LeaveContext> stateMachineBuilder = StateMachineBuilderFactory.create();
        //員工請假觸發事件
        //源狀態和目標狀態一致,我們可以用內部流轉表示
        stateMachineBuilder.internalTransition().within(ApplyStatusEnum.LEAVE_SUBMIT).on(Event.EMPLOYEE_SUBMIT).perform(doAction());
        //部門主管審批觸發事件(依賴上一個源狀態:LEAVE_SUBMIT)
        stateMachineBuilder.externalTransition().from(ApplyStatusEnum.LEAVE_SUBMIT).to(ApplyStatusEnum.LEADE_AUDIT_PASS).on(Event.DIRECTLEADER_AUDIT).when(checkIfPass()).perform(doAction());
        stateMachineBuilder.externalTransition().from(ApplyStatusEnum.LEAVE_SUBMIT).to(ApplyStatusEnum.LEADE_AUDIT_REFUSE).on(Event.DIRECTLEADER_AUDIT).when(checkIfNotPass()).perform(doAction());
        //hr事件觸發(依賴上一個源狀態:LEADE_AUDIT_PASS)
        stateMachineBuilder.externalTransition().from(ApplyStatusEnum.LEADE_AUDIT_PASS).to(ApplyStatusEnum.HR_PASS).on(Event.HR_AUDIT).when(checkIfPass()).perform(doAction());
        stateMachineBuilder.externalTransition().from(ApplyStatusEnum.LEADE_AUDIT_PASS).to(ApplyStatusEnum.HR_REFUSE).on(Event.HR_AUDIT).when(checkIfNotPass()).perform(doAction());

        return stateMachineBuilder.build(STATE_MACHINE_ID);

    }

}
我們執行stateMachine.showStateMachine()方法後,看下狀態機的詳細配置資訊:
github上fork2.4k,star8.7k的這款狀態機,原來長這樣!
 
上述頂部顯示的"leaveStateMachineId"是我們自定義的狀態機ID值。
我們在看內容左側部分即State值,詳細羅列了我們配置的狀態(包括from和to)。這裡我們知道總共有五種狀態分別是:LEAVE_SUBMIT、LEADE_AUDIT_PASS、LEADE_AUDIT_REFUSE、HR_PASS、HR_REFUSE。
​這裡我們著重看”LEADE_AUDIT_PASS、LEAVE_SUBMIT“兩部分。這兩個狀態都是代表了狀態機的源狀態,裡面包含了多個狀態流轉配置項即Transition部分。
Transition代表著狀態的流轉(分內部、外部流轉),當客戶端觸發相應事件,狀態機內部就能響應這個事件,一旦滿足檢查條件,最終就會返回目標狀態。
 

第三步:使用狀態機狀態機的使用分兩步走:

第一步:獲取狀態機例項

StateMachine<ApplyStatusEnum, Event, LeaveContext> stateMachine = StateMachineFactory.get("leaveStateMachineId");

第二步:向狀態機觸發一個fireEvent事件

 
ApplyStatusEnum state=stateMachine.fireEvent(ApplyStatusEnum.LEAVE_SUBMIT, Event.EMPLOYEE_SUBMIT,context);
 
fireEvent方法的第一個入參是源狀態(對應狀態機配置的from),第二個傳遞的是觸發的事件(對應配置的on),第三個引數是一個自定義上下文引數(對應配置的context)。
 
示例程式碼:
@DisplayName("員工提交請假申請單")
    @Test
    public void employSubmitRequest(){

        StateMachine<ApplyStatusEnum, Event, LeaveContext> stateMachine = StateMachineFactory.get("leaveStateMachineId");
        LeaveContext context = new LeaveContext();

        ApplyStatusEnum state=stateMachine.fireEvent(ApplyStatusEnum.LEAVE_SUBMIT, Event.EMPLOYEE_SUBMIT,context);

        Assert.assertEquals(ApplyStatusEnum.LEAVE_SUBMIT.getCode(),state.getCode());

    }

    @DisplayName("部門主管審批透過")
    @Test
    public void leaderAuditPass(){

        StateMachine<ApplyStatusEnum, Event, LeaveContext> stateMachine = StateMachineFactory.get("leaveStateMachineId");
        LeaveContext context = new LeaveContext();
        //主管審批透過
        context.setIdea(0);
        ApplyStatusEnum state=stateMachine.fireEvent(ApplyStatusEnum.LEAVE_SUBMIT, Event.DIRECTLEADER_AUDIT,context);
        Assert.assertEquals(ApplyStatusEnum.LEADE_AUDIT_PASS.getCode(),state.getCode());
    }

    @DisplayName("部門主管審批不透過")
    @Test
    public void leaderAuditNotPass(){

        StateMachine<ApplyStatusEnum, Event, LeaveContext> stateMachine = StateMachineFactory.get("leaveStateMachineId");
        LeaveContext context = new LeaveContext();
        //主管審批不透過
        context.setIdea(1);
        ApplyStatusEnum state=stateMachine.fireEvent(ApplyStatusEnum.LEAVE_SUBMIT, Event.DIRECTLEADER_AUDIT,context);
        Assert.assertEquals(ApplyStatusEnum.LEADE_AUDIT_REFUSE.getCode(),state.getCode());
    }


    @DisplayName("HR審批透過")
    @Test
    public void hrAuditPass(){

        StateMachine<ApplyStatusEnum, Event, LeaveContext> stateMachine = StateMachineFactory.get("leaveStateMachineId");
        LeaveContext context = new LeaveContext();
        //HR透過
        context.setIdea(0);
        ApplyStatusEnum state=stateMachine.fireEvent(ApplyStatusEnum.LEADE_AUDIT_PASS, Event.HR_AUDIT,context);
        Assert.assertEquals(ApplyStatusEnum.HR_PASS.getCode(),state.getCode());
    }

    @DisplayName("HR審批不透過")
    @Test
    public void hrAuditNotPass(){

        StateMachine<ApplyStatusEnum, Event, LeaveContext> stateMachine = StateMachineFactory.get("leaveStateMachineId");
        LeaveContext context = new LeaveContext();
        //HR審批不透過
        context.setIdea(1);
        ApplyStatusEnum state=stateMachine.fireEvent(ApplyStatusEnum.LEADE_AUDIT_PASS, Event.HR_AUDIT,context);
        Assert.assertEquals(ApplyStatusEnum.HR_REFUSE.getCode(),state.getCode());
    }
上面示例程式碼,我以員工請假流程為背景涉及部門審批流程,期間涉及如下幾個狀態:
LEAVE_SUBMIT(1,"已申請"),
LEADE_AUDIT_PASS(2,"直屬領導審批透過"),
LEADE_AUDIT_REFUSE(3,"直屬領導審批失敗"),
HR_PASS(4,"HR審批透過"),
HR_REFUSE(5,"HR審批拒絕");
我用cola-statemachine實現了整個生命週期的狀態流轉。完整程式碼我已開源在github上,感興趣的小夥伴可以自取。
 
github地址:https://github.com/TaoZhuGongBoy/enumstatemachine

架構設計

核心語義模型

github上fork2.4k,star8.7k的這款狀態機,原來長這樣!
 
我們一起看下狀態機的類關係圖。
 
一個狀態機(StateMachine)包含多個狀態(State)。一個狀態(State)包含多個流轉(Transition),一個Transition各包含一個Condition和Action。狀態State分源狀態(Source)和目標狀態(Target)。源狀態響應一個事件後,滿足一定觸發條件,經過流轉,執行Action動作,最後返回Target狀態。語義模型虛擬碼如下:
//StateMachine
public class StateMachineImpl<S,E,C> implements StateMachine<S, E, C> {

    private String machineId;
    //一個狀態機持有多個狀態(from、to)
    private final Map<S, State<S,E,C>> stateMap;

    ...
}

//State
public class StateImpl<S,E,C> implements State<S,E,C> {
    protected final S stateId;
    //同一個Event可以觸發多個Transition
    private Map<E, List<Transition<S, E,C>>> transitions = new HashMap<>();
    
    ...
}
//Transition
public class TransitionImpl<S,E,C> implements Transition<S,E,C> {
    //源狀態
    private State<S, E, C> source;
    //目標狀態
    private State<S, E, C> target;
    //事件
    private E event;
    //條件
    private Condition<C> condition;
    //動作
    private Action<S,E,C> action;
    
    ...
}

原始碼解析

原始碼部分,我將從客戶端執行fireEvent方法說起:
▲fireEvent方法
@Override
    public S fireEvent(S sourceStateId, E event, C ctx) {
        isReady();
        //根據sourceStateId找到符合條件的Transition
        Transition<S, E, C> transition = routeTransition(sourceStateId, event, ctx);

        if (transition == null) {
            Debugger.debug("There is no Transition for " + event);
            failCallback.onFail(sourceStateId, event, ctx);
            return sourceStateId;
        }
        //找到transition後執行transit方法(最終執行Action後返回目標State)
        return transition.transit(ctx, false).getId();
    }
fireEvent方法內部首先會根據原狀態ID去路由尋找具體的Transition,找到Transition後執行其transit方法,內部會執行perform函式,最終返回目標State。
▲我們再一起看下路由Transition部分即routeTransition方法:
/**
     * 路由Transition
     * @param sourceStateId 源狀態ID
     * @param event 事件
     * @param ctx 上下文引數
     * @return
     */
    private Transition<S, E, C> routeTransition(S sourceStateId, E event, C ctx) {
        //根據源狀態ID查詢源狀態例項
        State sourceState = getState(sourceStateId);

        //查詢源狀態例項下的流轉列表
        List<Transition<S, E, C>> transitions = sourceState.getEventTransitions(event);

        if (transitions == null || transitions.size() == 0) {
            return null;
        }

        Transition<S, E, C> transit = null;
        for (Transition<S, E, C> transition : transitions) {
            if (transition.getCondition() == null) {
                transit = transition;
            } else if (transition.getCondition().isSatisfied(ctx)) {
                //一旦匹配when函式內的觸發條件,返回transition
                transit = transition;
                break;
            }
        }

        return transit;
    }
▲最後我們再一起看下transition.transit方法細節
@Override
    public State<S, E, C> transit(C ctx, boolean checkCondition) {
        Debugger.debug("Do transition: "+this);
        this.verify();
        //checkCondition為false或不指定when觸發條件亦或匹配when觸發條件;都將執行自定義的perform函式
        if (!checkCondition || condition == null || condition.isSatisfied(ctx)) {
            //如果自定義的perform函式有指定,將執行perform函式
            if(action != null){
                action.execute(source.getId(), target.getId(), event, ctx);
            }
            return target;
        }

        Debugger.debug("Condition is not satisfied, stay at the "+source+" state ");
        return source;
    }

總結

好了,文章即將進入尾聲,讓我們一起做個總結:
 
前言部分,花了點時間簡單給大家介紹了一下,在多狀態屬性場景中,狀態機給我們帶來的諸多好處。
 
快速開始部分我比較細緻的給大家介紹了程式碼層面如何正確使用該元件,也給出了一個基於"員工請假"案例的示例程式碼,用狀態機實現內部審批狀態流轉。
 
架構設計部分我先給大家介紹了一下該元件的核心語義模型,用類圖來渲染。大家一看就能清楚知曉該狀態機的內部構造及內部元件與元件之間的關係。原始碼部分,我從客戶端觸發的fireEvent方法開始,給大家講解了一下它是如何從源狀態開始,響應事件,匹配指定的Transition,執行具體的action動作,返回目標狀態全過程。
 
希望看完本文,對你能幫助你更加深入的瞭解這款優秀的開源狀態機有所幫助,謝謝大家!
 
本文完。
 


 

寫到最後

作為996的程式設計師,寫這篇文章基本都是利用工作日下班時間和週六週日雙休的時間才最終成稿,比較不易。
 
如果你看了文章之後但凡對你有所幫助或啟發,真誠懇請幫忙關注一下作者,點贊、在看此文。你的肯定與讚美是我未來創作最強大的動力,我也將繼續前行,創作出更加優秀好的作品回饋給大家,在此先謝謝大家了!

關注我

如果這篇文章你看了對你有幫助或啟發,麻煩點贊、關注一下作者。你的肯定是作者創作源源不斷的動力。

公眾號

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

公眾號內回覆關鍵字“電子書”下載pdf格式的電子書籍(JAVAEE、Spring、JVM、併發程式設計、Mysql、Linux、kafka、分散式等)、“開發手冊”獲取阿里開發手冊2本、"面試"獲取面試PDF資料。

相關文章