草庐IT

github上fork2.4k,star8.7k的这款状态机,原来长这样!

座右铭 2023-04-16 原文
大家好,我是陶朱公Boy。

前言

上一篇文章《关于状态机的技术选型,最后一个真心好》我跟大家聊了一下关于”状态机“的话题。从众多技术选型中我也推荐了一款阿里开源的状态机—“cola-statemachine”。
 
于是就有小伙伴私信我,自己项目也考虑引入这款状态机,但网上资料实在太少,能不能系统的介绍一下如何使用这款工具。
读者有需求,是必须要满足的,谁叫
 
 
也是刚好前段时间因工作需要徒手写了一个简易版的工作流引擎(需要满足任意节点动态编排),里面涉及比较复杂的工作流状态流转,之前的if-else方案,实在搞的一团乱麻,自从引入了这款组件,一下子就解放了生产力。

▲原来的状态(if-else版本)

 
上面还只是if-else实现版本中很小一部分代码,基本都是多个switch嵌套,最里面的switch又涉及多个if-else判断,可维护性和健壮性不言而喻。。
 

▲改造后的状态(cola-statemachine版本)

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
 
那接下来,废话不多说,我们先实战一把,先学会如何使用它。如果你想更加深入、全面的去了解组件的架构,可以看下架构设计部分章节。
 

快速开始

接下来,我以一个员工请假案例作为背景,手把手带大家演示一下如何使用此组件。

第一步:项目中引入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()方法后,看下状态机的详细配置信息:
 
上述顶部显示的"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

架构设计

核心语义模型

 
我们一起看下状态机的类关系图。
 
一个状态机(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资料。

有关github上fork2.4k,star8.7k的这款状态机,原来长这样!的更多相关文章

  1. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  2. ruby-on-rails - 跳过状态机方法的所有验证 - 2

    当我的预订模型通过rake任务在状态机上转换时,我试图找出如何跳过对ActiveRecord对象的特定实例的验证。我想在reservation.close时跳过所有验证!叫做。希望调用reservation.close!(:validate=>false)之类的东西。仅供引用,我们正在使用https://github.com/pluginaweek/state_machine用于状态机。这是我的预订模型的示例。classReservation["requested","negotiating","approved"])}state_machine:initial=>'requested

  3. 语法类似于 GitHub Flavored Markdown 的 Ruby markdown 解释器? - 2

    我使用Jekyll运行博客,并认为我会解决RedcarpetMarkdown解释器,因为它是developedandusedbyGitHub.好吧,我只是碰巧遇到了一个错误,去检查问题,然后foundthis.Maintainersays,"Asyouprobablyhavenoticed(harharharhar)Idon'thavetimetomaintainRedcarpetanymore.It'snotapriorityforme(IfindMarkdownthoroughlyboring)andit'snotapriorityforGitHub,becausewenolong

  4. ruby - vagrant 从 github 安装插件 - 2

    我们正在使用Vagrant进行部署,我们最终希望将此集群部署在Rackspace上。vagrant-rackspace插件是一个自然的选择,但它有一些错误,这些错误未包含在最新的0.1.1版本中(notablythatvagrantprovisiondoesn'twork)。我已经在我的personalfork中解决了这个问题通过合并其他人的工作来对存储库进行改造。是否可以从github安装vagrant插件?显而易见的事情没有奏效:[unix]$vagrantplugininstallvagrant-rackspace--plugin-sourcehttps://github.com

  5. ruby-on-rails - 状态机、模型验证和 RSpec - 2

    这是我当前的类定义和规范:classEvent:not_starteddoevent:game_starteddotransition:not_started=>:in_progressendevent:game_endeddotransition:in_progress=>:finalendevent:game_postponeddotransition[:not_started,:in_progress]=>:postponedendstate:not_started,:in_progress,:postponeddovalidate:end_time_before_finalen

  6. ruby - 如何让 GitHub 页面使用 master 分支? - 2

    我有一个使用Jekyll托管在GitHub上的静态网站。问题是,我真的不需要master分支,因为存储库唯一包含的是网站。这样我就必须gitcheckoutgh-pages,然后gitmergemaster,然后gitpushorigingh-pages。有什么简单的方法可以摆脱gh-pages分支并直接从master推送? 最佳答案 Theproblemis,Idon'treallyneedthemasterbranch,astheonlythingtherepositorycontainsisthewebsite.Isthere

  7. ruby-on-rails - 你为 Rails 推荐哪个状态机插件? - 2

    关闭。这个问题不符合StackOverflowguidelines.它目前不接受答案。要求我们推荐或查找工具、库或最喜欢的场外资源的问题对于StackOverflow来说是偏离主题的,因为它们往往会吸引自以为是的答案和垃圾邮件。相反,describetheproblem以及迄今为止为解决该问题所做的工作。关闭9年前。Improvethisquestion我正在为Rails3/ActiveRecord项目寻找一个相对简单的状态机插件。我做了一些研究并提出了以下插件:转换:https://github.com/qoobaa/transitions从旧的ActiveRecord状态机库中提取

  8. ruby - 警告 : PATH set to RVM ruby but GEM_HOME and/or GEM_PATH not set, 请参阅 : https://github. com/wayneeseguin/rvm/issues/3212 - 2

    我每次打开终端时都会收到这个错误:警告:PATH设置为RVMruby​​但未设置GEM_HOME和/或GEM_PATH,请参阅:https://github.com/wayneeseguin/rvm/issues/3212这是在我最近安装zsh(oh-my-zsh)后开始发生的我不知道如何设置GEM_HOME和/或GEM_PATH的路径。 最佳答案 我也面临同样的问题,更改.zshrc中的以下行,exportPATH="/usr/local/heroku/bin:.........."到exportPATH="$PATH:/usr/

  9. ruby - github api v3 创建问题消息未找到 - 2

    当我尝试创建一个github问题时,它给出消息未找到回复。以及如何发送身份验证header。因为创建问题需要用户登录或验证curl-XPOST-i-d'{"title":"my-new-repo","body":"mynewissuedescription"}'https://api.github.com/repos/barterli/barter.li/issuesHTTP/1.1404NotFoundServer:GitHub.comDate:Wed,19Feb201407:11:33GMTContent-Type:application/json;charset=utf-8Sta

  10. ruby-on-rails - Fork、Ruby、ActiveRecord 和 Fork 上的文件描述符 - 2

    我知道当我们fork一个进程时,子进程会继承父进程的打开文件描述符和偏移量的副本。根据手册页,这指的是父级使用的相同文件描述符。基于以下程序中的理论puts"Process#{Process.pid}"file=File.open('sample','w')forked_pid=forkdosleep(10)puts"Writingtofilenow..."file.puts("HelloWorld.#{Time.now}")endfile.puts("Welcometowinterofmydiscontent#{Time.now}")file.closefile=nil问题一:当父进

随机推荐