Java状态机(Squirrel & cola)
主要关注如下3个状态机:
spring-statemachine、squirrel statemachine和cola-component-statemachine 。前两者是现在github上开源的最火的,也是对状态机功能实现的非常完整的项目,COLA-statemachine则是相对而言轻量且简单很多的实现
参考博客:
了解什么是状态模式(一种设计模式)
有关状态模式 和 COLA-state-machine的大致说明:关于状态机的技术选型,最后一个真心好!-腾讯云开发者社区-腾讯云 (tencent.com)
很好的介绍了三个状态机:java - 状态机引擎选型 - 个人文章 - SegmentFault 思否
Cola-State Machine
一些很好的参考博客:
聊聊Cola-StateMachine轻量级状态机的实现-腾讯云开发者社区-腾讯云 (tencent.com)
Cola-StateMachine状态机的实战使用-腾讯云开发者社区-腾讯云 (tencent.com)
保姆式教程!如何使用Cola-statemachine构建高可靠性的状态机 ? - 掘金 (juejin.cn)
有一步步的介绍,挺好的:COLA中的cola-statemachine状态机理解与使用例_cola statemachine-优快云博客
与前两者的区别
这个其实是最新的的一个轻量化的开源框架,与其他的状态机不同的是它的状态机内部并没有维护一个“current state”。所以这个框架中的状态机严格来说不是状态机,是一个状态转移指导器。
详细来说,Spring State Machine和Squirrel 中的状态机内部存储来当前的状态,在外界给一个 next state
的信号给状态机后自动的就会跳转到下一个状态。 但是Cola-State Machine的状态机在使用的时候需要给 next state
的信号和 current state
是什么,然后Cola-State Machine会告诉你下一个状态是什么。 状态机本身不维护“当前状态”这一信息,因此对于高并发场景非常好。
也就是说Cola-State Machine仅仅起到一个指导类型转换的功能,对于一种状态机,只需要实例化一个状态机就可以解决所有的状态转移需求。(10000个用于进程,Spring State Machine和Squirrel 需要有10000个状态机,Cola-State Machine只需要实例化一个状态机,当前状态什么的全部由用户进程自己保存。
缺点
太新了,现在的指导和开源项目实在是太少了,我什么没有找到专门的文档来,虽然轻量但是感觉需要实际用上的话可能难度比Squirrel还要大。
不过大概的学完的Squirrel之后回头来看发现Cola-State Machine 和 Squirrel其实在模型的创建和使用上还是非常的相似的。都是先生成一个builder然后建立状态转移,在状态转移的时候也能设置条件和要调用的函数,这一部分应该是Cola-State Machine参考的Squirrel的设计,用户使用的接口很类似。
Squirrel
英文文档:squirrel-foundation | Squirrel State Machine (hekailiang.github.io)
中文文档:Squirrel使用 | 城的灯 (yangguo.info)
状态机介绍:UML State Machine Diagrams - Overview of Graphical Notation (uml-diagrams.org)
有一个大好处,那就是它是一个成熟的框架,写代码的时候遇到了什么问题百度 google不出来直接去找ChatGPT大概率能够有一个还行的结果。所以最后选用了Squirrl实现任务。 但我实现完了之后看了一些Cola-State Machine的接口设计,感觉好像与Squirrl没啥很大的区别,对于一般的项目来说Cola-State Machine应该是更加适合的。
状态机的使用分为了构建和使用两种,这里直接给出个例子出来,以医学流程图这一需求为例,给定一张流程图,需要将这张流程图转变为状态机,期间涉及到了输入变量和根据输入变量的值进行条件转移。完全可以满足常见状态机的需求(不涉及状态并行什么的)。
注意给出的代码是不能直接跑的,只是一个使用的示例,帮助了解使用Squirrel状态机的步骤。
step1:构建状态、事件、条件
状态的话,随便写一个类即可,也不需要继承什么东西,这里的状态定义为: public abstract class BaseBox
。
public abstract class BaseBox {
private String ID;
private String text;
// .....
}
事件是用于控制转移的,两个状态之间的转移需要有对应的事件
public enum MedEvent {
TICK,
RESET,
}
上下文简单理解就是用于存储具体的变量的一个类,比如在TICK
这一事件发生时,变量a<10
时转移到一个状态,当a>=10
时到另一个状态中。这个变量就应该存在Context
(上下文)中。这个类不需要继承特殊的类,跟State 一样自己写一个就可以了。
public class MedContext {
private HashMap<String, Double> name2double;
public MedContext() {
this.name2double = new HashMap<>();
}
public MedContext(HashMap<String, Double> name2double) {
this.name2double = name2double;
}
public void addVar(String name, Double value) {
this.name2double.put(name, value);
}
public void addVar(HashMap<String, Double> var2value) {
for (String name : var2value.keySet()) {
this.name2double.put(name, var2value.get(name));
}
}
public String getVarValue(String name) {
if (!this.name2double.containsKey(name)) {
return null;
}
return this.name2double.get(name).toString();
}
}
条件是用与进一步控制状态转移的,比如在TICK
这一事件发生时,当变量a<10
时转移到一个状态,当a>=10
时到另一个状态中。需要继承Condition<Object>
,实现public boolean isSatisfied()
和 public boolean isSatisfied(Object context)
方法。
public class MedCondition implements Condition<Object> {
private String name;
private String boolExpression; // 用于判断的表达式
private static final String regex = "\\b[a-zA-Z_][a-zA-Z0-9_]*\\b";
private static final Pattern pattern = Pattern.compile(regex);
public MedCondition(String name) {
this.name = name;
}
public MedCondition(String name, String boolExpression) {
this.name = name;
this.boolExpression = boolExpression;
}
@Override
public boolean isSatisfied(Object context) {
// 注意此处的context类为Object,必须得要在底下进行强制类型转换,不能直接写自己定义的MedContext
MedContext medContext = (MedContext) context;
return evaluateExpression(this.boolExpression, medContext.getName2double());
}
@Override
public String name() {
return name;
}
}
使用如上的内容定义状态机。
@StateMachineParameters(stateType = BaseBox.class, eventType = MedEvent.class, contextType = MedContext.class)
public class MedStatemachine extends AbstractUntypedStateMachine {
private BaseBox initialState;
private String chartID;
// 可以在这里定义出需要的函数,此处是输出当前状态机的信息
public void printCurrentState() {
assert this.getCurrentState() instanceof BaseBox;
BaseBox currentState = (BaseBox) this.getCurrentState();
System.out.println(currentState.to_dict())
return;
}
}
step2:构建出状态机
// 自己写的一个用于构建状态机的类
public class MedStatemachineBuilder {
public static MedStatemachine build(String jsonString, Boolean isNeedFinalState) {
// step1:构造出一个状态机构建器
UntypedStateMachineBuilder builder = StateMachineBuilderFactory.create(MedStatemachine.class);
// step2:构造出所有的状态
// ...
for (int i = 0; i < chartObjectArray.size(); i++) {
BaseBox state = jsonNodeToBox(objJson); // BaseBox对应的是状态机的“状态”
if (isNeedFinalState && state.getBoxKind().equals(ConstBoxString.END_BOX)) {
// 定义final state,到了final state之后再调用状态机会报错,如果是一般状态的话则不会有任何变化
builder.defineFinalState(state);
} else {
builder.defineState(state);
}
}
// step3:构造出所有的转移
HashMap<String, ArrayList<String>> decisionBox2Condition = new HashMap<>();
for (int i = 0; i < chartObjectArray.size(); i++) {
BaseBox fromBox = newBaseBox;
BaseBox toBox = newBaseBox;
if (needCondition) {
// 有条件的转移
String condition = newCondition;
builder.externalTransition()
.from(fromBox)
.to(toBox)
.on(MedEvent.TICK)
.when(new MedCondition(newid, condition));
} else {
// 无条件转移,具体表现为没有When
builder.externalTransition()
.from(id2Box.get(edgeJson.getJSONObject("source").getString("cell")))
.to(id2Box.get(edgeJson.getJSONObject("target").getString("cell")))
.on(MedEvent.TICK);
}
}
// step4:完成构建状态机
MedStatemachine medStatemachine = (MedStatemachine) builder.newStateMachine(initialBox);
medStatemachine.setInitialState(initialBox);
return medStatemachine;
}
}
step3:使用状态机
两种,带有context
的和不带的,但是事件(Event
是一定要传的),主要是知道涉及到了when(condition)
的状态机必须要传入context
,这是condition
给出判断结果的数据源。
// 带有Context的
medStatemachine.fire(MedEvent.TICK, this.medContext);
// 不带Context的
this.medStatemachine.fire(MedEvent.RESET);