草庐IT

状态机的技术选型看这篇就够了,最后一个直叫好!!!

座右铭 2023-04-19 原文

前言

今天跟大家分享一个关于“状态机”的话题。状态属性在我们的现实生活中无处不在。比如电商场景会有一系列的订单状态(待支付、待发货、已发货、超时、关闭);员工提交请假申请会有申请状态(已申请、审核中、审核成功、审核拒绝、结束);差旅报销单会有单据审核状态(已提交、审核中、审核成功、退回、打款中、打款成功、打款失败、结束)等等。上述场景有一个共同问题:根据不同触发条件执行不同处理动作最后落地不同的状态。示例代码如下:

Integer status=0;
    if(condition1){
        status=1;
    }else if(condition2){
        status=2;
    }else if(condition3){
        status=3;
    }else if(condition4){
        status=4;
    }
复制代码

那我们最容易能想到的自然是if-else方案。那if-else方案会有什么问题呢?

主要有以下几点:

  • 复杂的业务流程,if.else代码几乎无法维护
  • 随着业务的发展,业务过程也需要变更及扩展,但if.else代码段已经无法支持
  • 没有可读性,变更风险特别大,可能会牵一发而动全身,线上事故层出不穷
  • 其他业务逻辑可能也会跟if-else代码块耦合在一起,带来更多的问题

状态机的出现就是用来解决上述问题的。在复杂多状态流转情况下,通过状态机的引入,我们希望相关代码可读性、扩展性能比if-else方案更好!

关于状态机

▲什么是状态机

状态机是有限状态自动机的简称。有限状态机(英语:finite-state machine,缩写:FSM)又称有限状态自动机(英语:finite-state automaton,缩写:FSA),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型。

关于有限的解释:也就是被描述的事物的状态的数量是有限的,例如开关的状态只有“开”和“关”两个;灯的状态只有“亮”和“灭”等等。

▲特点

一个状态机可以具有有限个特定的状态,它通常根据输入,从一个状态转移到另一个状态,不过也可能存在瞬时状态,而一旦任务完成,状态机就会立刻离开瞬时状态。每个状态根据不同的前置条件,会从当前状态流转至下一个状态。

▲作用

使用状态机来表达状态的流转,会使语义会更加清晰,会增强代码的可读性和可维护性。

▲适用场景

面对复杂的状态流转(一般是超过三个及以上的状态流转),那么还是比较建议用状态机来实现的。

各个状态机方案

▲枚举状态机

Java中的枚举是一个定义了一系列常量的特殊类(隐式继承自class java.lang.Enum)。枚举类型因为自身的线程安全性保障和高可读性特性,是简单状态机的首选。

关于线程安全说明
我们随便自定义一个枚举:

public enum OpinionsEnum {
    PASS,NOT_PASS
}
复制代码

试着反编译上述代码:

public final class OpinionsEnum extends java.lang.Enum<OpinionsEnum> {
  public static final OpinionsEnum PASS;
  public static final OpinionsEnum NOT_PASS;
  public static OpinionsEnum[] values();
  public static OpinionsEnum valueOf(java.lang.String);
  static {};
}
复制代码

通过反编译后的代码我们看到:OpinionsEnum它继承了java.lang.Enum类;class前的final标识告诉我们此枚举类不能被继承。

我们接着看它的两个属性:PASS、NOT_PASS。它们无一例外都经过了staic 的修饰,而我们知道staic修饰的属性会在类被加载之后就完成初始化,而这个过程是线程安全的。

示例代码:

public enum State {
    SUBMIT_APPLY {
        @Override
        State transition(String checkcondition) {
            System.out.println("员工提交请假申请单,同步流转到部门经理审批 参数 = " + checkcondition);
            return Department_MANAGER_AUDIT;
        }
    },
    Department_MANAGER_AUDIT {
        @Override
        State transition(String checkcondition) {
            System.out.println("部门经理审批完成,同步跳转到HR进行审批 参数 = " + checkcondition);
            return HR;
        }
    },
    HR {
        @Override
        State transition(String checkcondition) {
            System.out.println("HR完成审批,流转到结束组件, 参数 = " + checkcondition);
            return FINAL;
        }
    },
    FINAL {
        @Override
        State transition(String checkcondition) {
            System.out.println("流程结束, 参数 = " + checkcondition);
            return this;
        }
    };

    abstract State transition(String checkcondition);
}
复制代码
public class StatefulObjectDemo {
    private  State state;

    public StatefulObjectDemo() {
        state = State.SUBMIT_APPLY;
    }

    public void performRequest(String checkCondition) {
        state = state.transition(checkCondition);
    }

    public static void main(String[] args) {
      StatefulObjectDemo theObject = new StatefulObjectDemo();
        theObject.performRequest("arg1");
        theObject.performRequest("arg2");
        theObject.performRequest("arg3");
        theObject.performRequest("arg4");

    }
}
复制代码

输出:

员工提交请假申请单,同步流转到部门经理审批 参数 = arg1
部门经理审批完成,同步跳转到HR进行审批 参数 = arg2
HR完成审批,流转到结束组件, 参数 = arg3
流程结束, 参数 = arg4
复制代码

Java枚举有一个比较有趣的特性即它允许为实例编写方法,从而为每个实例赋予其行为。实现也很简单,定义一个抽象的方法即可,这样每个实例必须强制重写该方法。(见示例的transition方法)

▲状态模式实现的状态机

是什么

状态模式是编程领域特有的名词,是 23 种设计模式之一,属于行为模式的一种。

它允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。

作用状态模式的设计意图主要是为了解决两个主要问题:

  1. 当一个对象的内部状态改变时,它应该改变它的行为。

  2. 应独立定义特定于状态的行为。也就是说,添加新状态不应影响现有状态的行为。

类图:

类图

定义一个State接口,它可以有N个实现类,每个实现类需重写接口State定义的handle方法。它还有一个Context上下文类,内部持有一个State对象引用,外部状态发生改变(构造器内传入不同实现类),最终实现类自身行为动作也接着改变(实现类调用其自身的handle方法)。

Context示意图参考

用状态模式实现的代码示例:

public interface SwitchState {

    void handle();
}

public class TurnOffAction implements SwitchState{
    @Override
    public void handle() {
        System.out.println("关灯");
    }
}

public class TurnOnAction implements SwitchState{

    @Override
    public void handle() {
        System.out.println("开灯");
    }
}

public class Context {

    private SwitchState state;

    public Context(SwitchState state){
        this.state=state;
    }

    public void doAction(){
        state.handle();
    }
}
复制代码

输出

public class StatePatternDemo {

    @DisplayName("状态模式测试用例-开灯")
    @Test
    public void turnOn() {
        Context context = new Context(new TurnOnAction());
        context.doAction();
    }

输出:开灯

    @DisplayName("状态模式测试用例-关灯")
    @Test
    public void turnOff() {
        Context context = new Context(new TurnOffAction());
        context.doAction();
    }
}

输出:关灯
复制代码

大家看下这段示例代码:Context类有一个有参构造方法,参数类型是State,所以实例化对象的时候你可以传入State的不同的实现类。最终context.doAction()调用的是不同实现类的doAction方法。

▲开源实现

目前开源的状态机实现方案有spring-statemachine、squirrel-foundation、sateless4j等。其中spring-statemachine、squirrel-foundation在github上star和fock数稳居前二。

不过这些状态机普通使用下来普遍存在两个问题:

问题一:太复杂

因为基本囊括了UML State Machine上列举的所有功能,功能是强大了,但也搞得体积过于庞大、臃肿、很重。很多功能实际生产场景中根本用不到。

支持的高阶功能有:状态的嵌套(substate),状态的并行(parallel,fork,join)、子状态机等等。大家可以对照一下这些功能你是否用的到。

问题二:性能差

这些状态机都是有状态的(Stateful)的,有状态意味着多线程并发情况下如果是单个实例就容易出现线程安全问题。在如今的普遍分布式多线程环境中,你就不得不每次一个请求就创建一个状态机实例。但问题来了一旦碰到某些状态机它的构建过程很复杂,如果当下QPS又很高话,往往会造成系统的性能瓶颈。
在这里我给大家推荐一款阿里开源的状态机:cola-statemachine。github地址:github.com/alibaba/COL…
作者(张建飞:阿里高级技术专家)讲到面对复杂的状态流转,当时他们团队也想搞个状态机来减负,经过深思熟虑、不断类比之后他们考虑自研。希望能设计出一款功能相对简单、性能良好的开源状态机;最后命名为cola-component-statemachine(实现了内部DSL语法;目前最新版本:4.3.1)

示例代码:

//构建一个状态机(生产场景下,生产场景可以直接初始化一个Bean)
StateMachineBuilder<StateMachineTest.ApplyStates, StateMachineTest.ApplyEvents, Context> builder = StateMachineBuilderFactory.create();
      //外部流转(两个不同状态的流转)
      builder.externalTransition()
        .from(StateMachineTest.ApplyStates.APPLY_SUB)//原来状态
        .to(StateMachineTest.ApplyStates.AUDIT_ING)//目标状态
        .on(StateMachineTest.ApplyEvents.SUBMITING)//基于此事件触发
        .when(checkCondition1())//前置过滤条件
        .perform(doAction());//满足条件,最终触发的动作
复制代码

上述代码先构建了一个状态机实例:from和to分别定义了源状态和目标状态,on定义了一个事件(状态机基于事件触发)当状态机匹配到指定的事件后,会进行条件过滤,如果满足指定条件,就会执行perform定义的动作函数,最终状态会从from内的源状态变成to定义的目标状态。

我们一起来看看客户端是怎么触发自定义的状态机的:

复制代码
StateMachine<StateMachineTest.ApplyStates, StateMachineTest.ApplyEvents, Context> stateMachine = builder.build("ChoiceConditionMachine");
//fireEvent发送一个事件;对应上面示例代码的ApplyEvents.SUBMITING.
StateMachineTest.ApplyStates target1 = stateMachine.fireEvent(StateMachineTest.ApplyStates.APPLY_SUB, StateMachineTest.ApplyEvents.SUBMITING, new Context("pass"));
输出:
from:APPLY_SUB to:AUDIT_ING on:SUBMITING condition:pass
复制代码

我把上述三款状态机的示例代码都放在了github上,有兴趣的小伙伴可以自行查阅。

github地址:

github.com/TaoZhuGongB…

总结

好了,此篇文章即将进入尾声,让我们一起来做个总结。

为什么引入状态机?

前言部分我也提到了在面对复杂的状态流转场景下if-else方案主要容易引起可读性、可扩展、易出错等问题,所以引入状态机主要为了降低这些风险。

状态机的实现方案对比:

状态机实现方案我举例了Java枚举、状态模式、开源状态机等几个实现方案。状态模式的问题是它需要定义接口、和实现类还附带一个Context上下文类,编码层面比较复杂。Java枚举版的状态机主要问题是扩展粒度不够基本都是线性扩展,封装在一个类中,太复杂的状态流转这个类也会变得臃肿不堪,维护性变低。
所以也推荐了一款比较理想的开源状态机实现--cola-component-statemachine。它使用相当简单,因为实现了内部DSL,所以可读性很强,当然扩展性也比较不错。

公众号:

里面不仅汇集了硬核的干货技术、还汇集了像左耳朵耗子、张朝阳总结的高效学习方法论、职场升迁窍门、软技能。希望能辅助你达到你想梦想之地!

公众号内回复关键字电子书”下载pdf格式的电子书籍(并发编程、JVM、MYSQL、JAVAEE、Linux、Spring、分布式等,你想要的都有!)、“开发手册”获取阿里开发手册2本、"面试"获取面试PDF资料。

有关状态机的技术选型看这篇就够了,最后一个直叫好!!!的更多相关文章

  1. ruby - 在 Ruby 程序执行时阻止 Windows 7 PC 进入休眠状态 - 2

    我需要在客户计算机上运行Ruby应用程序。通常需要几天才能完成(复制大备份文件)。问题是如果启用sleep,它会中断应用程序。否则,计算机将持续运行数周,直到我下次访问为止。有什么方法可以防止执行期间休眠并让Windows在执行后休眠吗?欢迎任何疯狂的想法;-) 最佳答案 Here建议使用SetThreadExecutionStateWinAPI函数,使应用程序能够通知系统它正在使用中,从而防止系统在应用程序运行时进入休眠状态或关闭显示。像这样的东西:require'Win32API'ES_AWAYMODE_REQUIRED=0x0

  2. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  3. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  4. ruby-on-rails - 渲染另一个 Controller 的 View - 2

    我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>

  5. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  6. ruby - 为什么 SecureRandom.uuid 创建一个唯一的字符串? - 2

    关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?

  7. 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

  8. ruby-on-rails - Rails - 从另一个模型中创建一个模型的实例 - 2

    我有一个正在构建的应用程序,我需要一个模型来创建另一个模型的实例。我希望每辆车都有4个轮胎。汽车模型classCar轮胎模型classTire但是,在make_tires内部有一个错误,如果我为Tire尝试它,则没有用于创建或新建的activerecord方法。当我检查轮胎时,它没有这些方法。我该如何补救?错误是这样的:未定义的方法'create'forActiveRecord::AttributeMethods::Serialization::Tire::Module我测试了两个环境:测试和开发,它们都因相同的错误而失败。 最佳答案

  9. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

    我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

  10. ruby - 一个 YAML 对象可以引用另一个吗? - 2

    我想让一个yaml对象引用另一个,如下所示:intro:"Hello,dearuser."registration:$introThanksforregistering!new_message:$introYouhaveanewmessage!上面的语法只是它如何工作的一个例子(这也是它在thiscpanmodule中的工作方式。)我正在使用标准的ruby​​yaml解析器。这可能吗? 最佳答案 一些yaml对象确实引用了其他对象:irb>require'yaml'#=>trueirb>str="hello"#=>"hello"ir

随机推荐