状态机的技术选型,yyds!
前言
今天跟大家分享一个关于“状态机”的话题。状态属性在我们的现实生活中无处不在。比如电商场景会有一系列的订单状态(待支付、待发货、已发货、超时、关闭);员工提交请假申请会有申请状态(已申请、审核中、审核成功、审核拒绝、结束);差旅报销单会有单据审核状态(已提交、审核中、审核成功、退回、打款中、打款成功、打款失败、结束)等等。上述场景有一个共同问题:根据不同触发条件执行不同处理动作最后落地不同的状态。示例代码如下:
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 种设计模式之一,属于行为模式的一种。
它允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
作用状态模式的设计意图主要是为了解决两个主要问题:
当一个对象的内部状态改变时,它应该改变它的行为。
应独立定义特定于状态的行为。也就是说,添加新状态不应影响现有状态的行为。
类图:
类图
定义一个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地址:
总结
好了,此篇文章即将进入尾声,让我们一起来做个总结。
为什么引入状态机?
前言部分我也提到了在面对复杂的状态流转场景下if-else方案主要容易引起可读性、可扩展、易出错等问题,所以引入状态机主要为了降低这些风险。
状态机的实现方案对比:
状态机实现方案我举例了Java枚举、状态模式、开源状态机等几个实现方案。状态模式的问题是它需要定义接口、和实现类还附带一个Context上下文类,编码层面比较复杂。Java枚举版的状态机主要问题是扩展粒度不够基本都是线性扩展,封装在一个类中,太复杂的状态流转这个类也会变得臃肿不堪,维护性变低。
所以也推荐了一款比较理想的开源状态机实现--cola-component-statemachine。它使用相当简单,因为实现了内部DSL,所以可读性很强,当然扩展性也比较不错。
公众号:

里面不仅汇集了硬核的干货技术、还汇集了像左耳朵耗子、张朝阳总结的高效学习方法论、职场升迁窍门、软技能。希望能辅助你达到你想梦想之地!
公众号内回复关键字“电子书”下载pdf格式的电子书籍(并发编程、JVM、MYSQL、JAVAEE、Linux、Spring、分布式等,你想要的都有!)、“开发手册”获取阿里开发手册2本、"面试"获取面试PDF资料。
状态机的技术选型,yyds!的更多相关文章
- Unity外包团队:关于手机unity游戏开发的技术选型
技术选型 Unity引擎内置了多人联机的解决方案,涵盖了从最底层的网络数据传输,到不同玩家之间的消息发送,再到游戏大厅这样的高级功能.考虑到Unity官方提供的云服务(Internet Service ...
- #数据技术选型#即席查询Shib+Presto,集群任务调度HUE+Oozie
郑昀 创建于2014/10/30 最后更新于2014/10/31 一)选型:Shib+Presto 应用场景:即席查询(Ad-hoc Query) 1.1.即席查询的目标 使用者是产品/运营/销售 ...
- 老王讲自制RPC框架.(一.前言与技术选型)
(#)背景 随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进. 单一应用架构 当网站流量很小时,只 ...
- Atitit 开发2d游戏的技术选型attilax总结
Atitit 开发2d游戏的技术选型attilax总结 1.1. 跨平台跨平台:一定要使用跨平台的gui技术,目前最好的就是h5(canvas,webgl,dom) +js了..1 1.2. 游戏前后 ...
- 《2016ThoughtWorks技术雷达峰会----js爆炸下的技术选型》
JS爆炸下的技术选型 刘尚奇 ThoughtWorks, 高级咨询师 JS每6个星期出现一个新框架,那么如何进行JS的选型.以下从四个方面来分析. 1.工具 NPM for all the t ...
- 手机web站点和手机app 技术选型的困惑于思考
今年一直在关注移动端技术的发展,自己也用博客园的rss接口玩了半年,关于技术选型的困惑和大家说说 一 趋势 随着手机硬件不断的升级,外加4g牌照的发放,不出2年时间移动端web站点和手机app一定会进 ...
- atitit.技术选型方法总结为什么java就是比.net有前途
atitit.技术选型方法总结为什么java就是比.net有前途 #----按照不同的需要有不铜的法... 一般有开发效率,稳定性上的需要.. 作者 老哇的爪子 Attilax 艾龙, EMAIL: ...
- 消息中间件的技术选型心得-RabbitMQ、ActiveMQ和ZeroMQ
消息中间件的技术选型心得-RabbitMQ.ActiveMQ和ZeroMQ 作者:chszs,转载需注明.博客主页:http://blog.csdn.net/chszs RabbitMQ.Active ...
- AutoLayout技术选型和应用
前言:这篇文章是笔者在项目中对布局技术进行技术选型和应用的相关介绍,供大家参考. && [self.buttonscount] > 0) { UIButton *button = ...
随机推荐
- Postman中的Pre-request Scrip详解
Postman中的Pre-request Scrip详解 一.Pre-request Scrip的简介 1.Pre-request Script是在请求发送之前需要执行的代码片段: 2.请求参数中包含 ...
- std::atomic和std::mutex区别
std::atomic介绍 模板类std::atomic是C++11提供的原子操作类型,头文件 #include<atomic>.在多线程调用下,利用std::atomic可实 ...
- 轻量级RTSP服务和内置RTSP网关有什么不同?
好多开发者疑惑,什么是内置RTSP网关,和轻量级RTSP服务又有什么区别和联系?本文就以上问题,做个简单的介绍: 轻量级RTSP服务 为满足内网无纸化/电子教室等内网超低延迟需求,避免让用户配置单独的 ...
- 快速排序C语言版图文详解
算法原理:选一个数位基准,将序列分成两个部分,一边全是比它小序列,另一边全是比它大序列.然后再分别对比他小的序列和比再次进行基准分割.依次分割下去,得到一个有序的队列. 原理图示: 编辑 编辑 ...
- Windows磁盘容量差异
如果足够细心,你就能发现计算机管理里面显示的容量和我的电脑里面磁盘容量的显示有差异.我的电脑中显示的总会少一点. https://www.cnblogs.com/qishine/p/12125329. ...
- 《Win10——如何设置开机自启动项》
Win10--如何设置开机自启动项 1. 为需要自启动的程序创建快捷方式. 2. Win+R输入"shell:startup",按下回车键出现一个文件夹. 3. 将快捷 ...
- Mybatis 一级缓存和二级缓存原理区别 (图文详解)
Java面试经常问到Mybatis一级缓存和二级缓存,今天就给大家重点详解Mybatis一级缓存和二级缓存原理与区别@mikechen Mybatis缓存 缓存就是内存中的数据,常常来自对数据库查询结 ...
- 【项目实战】自备相机+IMU跑通Vins-Mono记录
前言 初次接触SLAM,公司要求用自己的设备来跑通vinsmono这个程序,虽然已经跑通了别人的数据包,但是真正自己上手来运行这个程序,发现真的是困难重重,特意在此记载下来整个过程,以供大家参考. 我 ...
- Kubernetes 监控:Prometheus Adpater =》自定义指标扩缩容
使用 Kubernetes 进行容器编排的主要优点之一是,它可以非常轻松地对我们的应用程序进行水平扩展.Pod 水平自动缩放(HPA)可以根据 CPU 和内存使用量来扩展应用,前面讲解的 HPA 章节 ...
- 通过helm搭建Harbor
文章转载自:http://www.mydlq.club/article/66/ 系统环境: kubernetes 版本:1.20.1 Traefik Ingress 版本:2.4.3 Harbor C ...