状态机
引言
在业务处理中, 经常需要处理业务对象的状态转换, 比如 bug 状态管理、订单状态管理等, 这类问题可依照状态的复杂度,可以有不同的解决方案。
-
简单的顺序状态管理
如果状态数量很少,同时状态是按照一个方向流转,就可以归到这类。 对应的解决方案很简单, 在业务对象中增加一个表示状态的枚举字段即可。 -
复杂的 BPM 状态管理
这里复杂的状态管理, 典型特征是包含 BPM 的 split/join, 比如合同的会签, 对应的解决方案就使用重量级的工作流引擎, 比如 Java 的 flowable、camunda、 activiti, C#的 Elsa Workflows 等。 -
中等复杂的状态管理
我将不含 BPM 的 split/join 情形的都归到中等复杂程度,最佳解决方案是使用状态机,在每个业务对象中内嵌一个状态机,由状态机复杂状态转换。
对于简单的状态管理,往往后期会逐渐会变得不那么简单,所以, 对于简单的状态管理,我也推荐使用状态机,甚至这么说,只要不涉及到 BPM split/join, 使用状态机都是最佳方案。
状态机的几个概念
-
状态转换 transition
目标状态仅由前置状态经过一个事件触发实现状态转换。 -
守卫条件 Guard condition
从前置状态可以无条件地通过一个触发条件转换到目标状态,也可以有条件地通过触发事件转换。
这里的条件在状态机中被叫做 guard condition。
需要说明的是, guard condition 不应该被理解成状态转换的 pre check 条件,而是不同的目标状态的的区分条件。
(A) ----triggerB (condition1) --> (B1)
|
| -----triggerB (condition2) --> (B2)
Java 状态机
我比较喜欢 C# Stateless 类库,在 Java 中也有很多状态机类库, 比如 Squirrel、Spring Statemachine、EasyFlow、Apache SCXML。 和 C# Stateless 风格最像的是 Squirrel, C# Stateless 有的功能它基本都有,也可以导出 graphviz 流程图,只是缺少导出 mermaid 流程图,考虑到 mermaid 流程DSL非常简单, 自己实现也很简单。
squirrel 相比 Stateless 有更丰富的表达形式, 支持声明式 和 fluent api 多种表达形式, 而且事件有更多的回调event slot。
Squirrel:https://github.com/hekailiang/squirrel
C# Stateless 状态机开源库
Stateless github 是 C#中最流行的状态机实现,功能非常强大:
- 支持守卫 condition
- 支持带参数的 trigger
- 内建很多事件可供绑定
- 可进行触发检查
- 支持父子状态
Tips: 使用 PermitIf() 启用守卫 condition时,可以增加一个说明类型参数, 该参数可以在导出流程图时输出到trigger的label上,这个说明对于理解状态转换非常有帮助。 无条件的状态转换没有办法传入这个说明参数, 所以即使是无条件的状态转换,推荐增加一个恒成立的守卫 condition,以便能加上这个说明参数。
C# 示例代码(基于Stateless)
using System.Runtime.CompilerServices;
using Stateless;
namespace ConsoleApp1
{
internal class Program
{
private static void Main(string[] args)
{
// Console.WriteLine("==================");
// Order goodsOrder = new Order(OrderState.Created);
// goodsOrder.isVirtualOrder = false;
// goodsOrder.pay("OpeatorA");
// Console.WriteLine("==================");
// Order virtualOrder = new Order(OrderState.Created);
// virtualOrder.isVirtualOrder = true;
// virtualOrder.pay("OpeatorB");
// Console.WriteLine("==================");
// Order paidOrder = new Order(OrderState.Paid);
// paidOrder.ship();
// Console.WriteLine("==================");
// Order shippedOrder = new Order(OrderState.Shipped);
// shippedOrder.ship();
//导出 graphviz 流程图(.dot文件)
//最好使用绑定状态机最开始的state对象来导出,这样流程图看起来更加漂亮, 当然也可以任意状态的状态机导出。
// Console.WriteLine("==================");
// Order oneOrder = new Order(OrderState.Created);
// Console.WriteLine(oneOrder.exportToGraphviz());
//导出 mermaid 流程图
//最好使用绑定状态机最开始的state对象来导出,这样流程图看起来更加漂亮, 当然也可以任意状态的状态机导出。
Console.WriteLine("==================");
Order oneOrder2 = new Order(OrderState.Shipped);
Console.WriteLine(oneOrder2.exportToMermaid());
}
}
public class Order
{
public bool isVirtualOrder { get; set; }
private string _operatorName;
/// <summary>
/// 状态机对象应该从属于业务对象
/// </summary>
private StateMachine<OrderState, OrderTrigger> _stateMachine;
/// <summary>
/// 声明一个带参数的trigger
/// 状态机只允许为一个trigger 枚举值绑定一个带参数的trigger
/// </summary>
private StateMachine<OrderState, OrderTrigger>.TriggerWithParameters<string> _payTrigger;
public Order(OrderState orderState)
{
// 初始化状态机对象
// 需要指定状态机的 initialState, 需要注意的是, 它并不是一定是整个流程中最开始的状态, 而是本业务对象的当前状态
_stateMachine = new StateMachine<OrderState, OrderTrigger>(orderState);
_stateMachine.OnTransitionCompleted(
(transition) => { Console.WriteLine($"Source:{transition.Source} Destination:{transition.Destination}"); }
);
//非传参trigger,直接使用 trigger enum 即可
//传参trigger, 必须声明为 TriggerWithParameters 类型, 参数需要通过 Fire() 函数传入, 需要在“目标State”的Configuration下通过 OnEntryFrom() 来接收参数
_payTrigger = _stateMachine.SetTriggerParameters<string>(OrderTrigger.Pay);
//演示使用守卫条件guard condition的状态转换,PermitIf()中的条件应该是互斥的.
//最好要为守卫条件设置一个描述信息,这个描述信息将会体现到导出流程图的transition label上
_stateMachine.Configure(OrderState.Created)
.PermitIf(OrderTrigger.Pay, OrderState.Paid, () => { return isVirtualOrder == false; }, "Non virtual order")
.PermitIf(OrderTrigger.Pay, OrderState.Shipped, () => { return isVirtualOrder == true; }, "Virtual order");
_stateMachine.Configure(OrderState.Paid)
//用来接收Trigger传参的OnEntryFrom()调用, 要放到“目标State”的Configuration下
.OnEntryFrom(_payTrigger, operatorName => { _operatorName = operatorName; Console.WriteLine($"{_operatorName}"); })
.Permit(OrderTrigger.Ship, OrderState.Shipped);
_stateMachine.Configure(OrderState.Shipped)
.Permit(OrderTrigger.Confirm, OrderState.Closed);
}
public void pay(string operatorName)
{
Console.WriteLine(_stateMachine.State);
_stateMachine.Fire(_payTrigger, operatorName);
Console.WriteLine(_stateMachine.State);
}
public void ship()
{
if (_stateMachine.CanFire(OrderTrigger.Ship))
{
Console.WriteLine(_stateMachine.State);
_stateMachine.Fire(OrderTrigger.Ship);
Console.WriteLine(_stateMachine.State);
}
else
{
Console.WriteLine($"Cannot fire {OrderTrigger.Ship} in state {_stateMachine.State}");
}
}
/// <summary>
///导出 graphviz 流程图(.dot文件), 可以使用 VS code的 Graphviz Interactive Preview 插件预览.dot 文件流程图
///最好使用绑定状态机最开始的state对象来导出,这样流程图看起来更加漂亮, 当然也可以任意状态的状态机导出。
/// </summary>
/// <returns></returns>
public string exportToGraphviz()
{
return Stateless.Graph.UmlDotGraph.Format(_stateMachine.GetInfo());
}
/// <summary>
/// 将输出内容加到 markdown 的 code block 中, 形式为:
/// ```mermaid 流程内容 ```
/// VS code 可以安装 Markdown Preview Mermaid Support进行预览, 或者通过网站 https://mermaid.live/ 浏览
/// </summary>
/// <returns></returns>
public string exportToMermaid()
{
return Stateless.Graph.MermaidGraph.Format(_stateMachine.GetInfo());
}
}
public enum OrderState
{ Created, Paid, Shipped, Closed }
public enum OrderTrigger
{ Pay, Ship, Confirm }
}
输出的graphviz流程图:

输出的mermaid流程图:

Java 示例代码(基于 Squirrel )
pom.xml 内容:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- Spring Boot 父级依赖,提供默认配置和依赖管理 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version> <!-- 最后一个支持JDK 8的稳定版本 -->
<relativePath/> <!-- 从仓库查找,不从本地路径 -->
</parent>
<!-- 项目基本信息 -->
<groupId>com.example</groupId>
<artifactId>springboot-cli-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Spring Boot CLI Demo</name>
<description>最简单的Spring Boot命令行程序</description>
<!-- 属性配置 -->
<properties>
<!-- 指定Java版本为1.8 -->
<java.version>1.8</java.version>
<!-- 指定项目编码 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- Maven编译插件版本 -->
<maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
</properties>
<!-- 项目依赖 -->
<dependencies>
<!-- Spring Boot 核心启动器,包含自动配置、日志、YAML支持等 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- squirrel 状态机 -->
<dependency>
<groupId>org.squirrelframework</groupId>
<artifactId>squirrel-foundation</artifactId>
<version>0.3.6</version>
</dependency>
<!-- 测试依赖(可选) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- 构建配置 -->
<build>
<!-- 插件配置 -->
<plugins>
<!-- Spring Boot Maven插件:支持打包可执行JAR、运行应用等 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${project.parent.version}</version>
<configuration>
<!-- 指定主类 -->
<mainClass>SpringBootCli.SpringBootCli.App</mainClass>
</configuration>
<executions>
<execution>
<goals>
<!-- 将应用打包成可执行JAR -->
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Maven编译插件(可选,父POM已配置) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
</build>
<!-- 仓库配置(国内用户可添加阿里云镜像加速) -->
<repositories>
<repository>
<id>aliyun</id>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>aliyun</id>
<url>https://maven.aliyun.com/repository/public</url>
</pluginRepository>
</pluginRepositories>
</project>
Java主程序内容
package SpringBootCli.SpringBootCli;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.squirrelframework.foundation.component.SquirrelProvider;
import org.squirrelframework.foundation.fsm.Condition;
import org.squirrelframework.foundation.fsm.DotVisitor;
import org.squirrelframework.foundation.fsm.StateMachineBuilder;
import org.squirrelframework.foundation.fsm.StateMachineBuilderFactory;
import org.squirrelframework.foundation.fsm.impl.AbstractStateMachine;
/**
* Spring Boot 应用程序入口类
*
* @SpringBootApplication 注解包含: - @Configuration: 声明为配置类
* - @EnableAutoConfiguration: 启用自动配置 - @ComponentScan:
* 启用组件扫描
*/
@SpringBootApplication
public class App implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
@Override
public void run(String... args) throws Exception {
System.out.println("==================");
Order goodsOrder = new Order(OrderState.Created);
goodsOrder.getContext().setVirtualOrder(false);
goodsOrder.pay("OperatorA");
System.out.println("==================");
Order virtualOrder = new Order(OrderState.Created);
virtualOrder.getContext().setVirtualOrder(true);
virtualOrder.pay("OperatorB");
System.out.println("==================");
Order paidOrder = new Order(OrderState.Paid);
paidOrder.ship();
System.out.println("==================");
Order shippedOrder = new Order(OrderState.Shipped);
shippedOrder.ship();
System.out.println("==================");
Order oneOrder = new Order(OrderState.Created);
System.out.println(oneOrder.exportToGraphviz());
}
}
//----------------------------------------------------------
//状态枚举
//----------------------------------------------------------
enum OrderState {
Created, Paid, Shipped, Closed
}
//----------------------------------------------------------
//触发器(事件)枚举
//----------------------------------------------------------
enum OrderTrigger {
Pay, Ship, Confirm
}
//----------------------------------------------------------
//上下文对象 —— 在状态机转换时携带业务信息
//----------------------------------------------------------
class OrderContext {
private boolean isVirtualOrder;
private String operatorName;
public boolean isVirtualOrder() {
return isVirtualOrder;
}
public void setVirtualOrder(boolean virtualOrder) {
this.isVirtualOrder = virtualOrder;
}
public String getOperatorName() {
return operatorName;
}
public void setOperatorName(String operatorName) {
this.operatorName = operatorName;
}
}
//----------------------------------------------------------
//状态机定义类
//----------------------------------------------------------
class OrderStateMachine extends AbstractStateMachine<OrderStateMachine, OrderState, OrderTrigger, OrderContext> {
/**
* afterTransitionCompleted 在 transition 转换过程中被自动调用, 这时候当前状态还是 from state
*/
@Override
protected void afterTransitionCompleted(OrderState from, OrderState to, OrderTrigger event, OrderContext context) {
System.out.println(String.format("afterTransitionCompleted() Current state:%s", this.getCurrentState()));
System.out.println(String.format("afterTransitionCompleted(): Source:%s , Destination:%s", from, to));
}
/**
* afterTransitionEnd 在 transition 转换完成后被自动调用, 这时候当前状态已经是 to state
*/
@Override
protected void afterTransitionEnd(OrderState from, OrderState to, OrderTrigger event, OrderContext context) {
System.out.println(String.format("afterTransitionEnd() Current state:%s", this.getCurrentState()));
System.out.println(String.format("afterTransitionEnd(): Source:%s , Destination:%s", from, to));
}
/**
* callMethod 需要在 transition 时主动被调用,它被调用的时间点要早于 afterTransitionCompleted
*
* @param from
* @param to
* @param event
* @param context
*/
void callMethod(OrderState from, OrderState to, OrderTrigger event, OrderContext context) {
System.out.println(String.format("callMethod() Current state:%s", this.getCurrentState()));
System.out.println(String.format("callMethod(): Source:%s , Destination:%s", from, to));
}
}
//----------------------------------------------------------
//业务类 Order
//----------------------------------------------------------
class Order {
private final OrderContext context;
private final StateMachineBuilder<OrderStateMachine, OrderState, OrderTrigger, OrderContext> builder;
private final OrderStateMachine stateMachine;
protected void stateMachinePerformAction(OrderState from, OrderState to, OrderTrigger event, OrderContext context) {
System.out.println(String.format("1Source:%s 1Destination:%s%n", from, to));
}
public Order(OrderState initialState) {
this.context = new OrderContext();
// 1️. 创建状态机构建器
builder = StateMachineBuilderFactory.create(OrderStateMachine.class, OrderState.class, OrderTrigger.class,
OrderContext.class);
// 2️. 状态机配置
builder.externalTransition().from(OrderState.Created).to(OrderState.Paid).on(OrderTrigger.Pay)
.when(new Condition<OrderContext>() {
@Override
public boolean isSatisfied(OrderContext ctx) {
return !ctx.isVirtualOrder();
}
@Override
public String name() {
return "Non virtual order";
}
}).callMethod("callMethod");
builder.externalTransition().from(OrderState.Created).to(OrderState.Shipped).on(OrderTrigger.Pay)
.when(new Condition<OrderContext>() {
@Override
public boolean isSatisfied(OrderContext ctx) {
return ctx.isVirtualOrder();
}
@Override
public String name() {
return "Virtual order";
}
}).callMethod("callMethod");
builder.externalTransition().from(OrderState.Paid).to(OrderState.Shipped).on(OrderTrigger.Ship)
.callMethod("callMethod");
builder.externalTransition().from(OrderState.Shipped).to(OrderState.Closed).on(OrderTrigger.Confirm)
.callMethod("callMethod");
// 3️. 创建状态机实例, 并启动
stateMachine = builder.newStateMachine(initialState);
stateMachine.start(context);
}
public OrderContext getContext() {
return context;
}
// 支付事件
public void pay(String operatorName) {
context.setOperatorName(operatorName);
System.out.println("before fire:" + stateMachine.getCurrentState());
try {
stateMachine.fire(OrderTrigger.Pay, context);
System.out.println("after fire:" + stateMachine.getCurrentState());
} catch (Exception e) {
e.printStackTrace();
}
}
// 发货事件
public void ship() {
OrderState current = stateMachine.getCurrentState();
if (stateMachine.canAccept(OrderTrigger.Ship)) {
System.out.println("before fire:" + stateMachine.getCurrentState());
stateMachine.fire(OrderTrigger.Ship, context);
System.out.println("after fire:" + stateMachine.getCurrentState());
} else {
System.out.printf("Cannot fire %s in state %s%n", OrderTrigger.Ship, current);
}
}
// 导出为 Graphviz DOT 格式
public String exportToGraphviz() {
// // SCXML format
// SCXMLVisitor scxmlVvisitor = SquirrelProvider.getInstance().newInstance(SCXMLVisitor.class);
// this.stateMachine.accept(scxmlVvisitor);
// String scxmlFileName = "";
// scxmlVvisitor.convertSCXMLFile(scxmlFileName, true);
// Dot format
DotVisitor dotVisitor = SquirrelProvider.getInstance().newInstance(DotVisitor.class);
this.stateMachine.accept(dotVisitor);
String dotFileName = "d://1.dot";
dotVisitor.convertDotFile(dotFileName);
return "";
}
}

浙公网安备 33010602011771号