状态机

引言

在业务处理中, 经常需要处理业务对象的状态转换, 比如 bug 状态管理、订单状态管理等, 这类问题可依照状态的复杂度,可以有不同的解决方案。

  1. 简单的顺序状态管理
    如果状态数量很少,同时状态是按照一个方向流转,就可以归到这类。 对应的解决方案很简单, 在业务对象中增加一个表示状态的枚举字段即可。

  2. 复杂的 BPM 状态管理
    这里复杂的状态管理, 典型特征是包含 BPM 的 split/join, 比如合同的会签, 对应的解决方案就使用重量级的工作流引擎, 比如 Java 的 flowable、camunda、 activiti, C#的 Elsa Workflows 等。

  3. 中等复杂的状态管理
    我将不含 BPM 的 split/join 情形的都归到中等复杂程度,最佳解决方案是使用状态机,在每个业务对象中内嵌一个状态机,由状态机复杂状态转换。

对于简单的状态管理,往往后期会逐渐会变得不那么简单,所以, 对于简单的状态管理,我也推荐使用状态机,甚至这么说,只要不涉及到 BPM split/join, 使用状态机都是最佳方案。

状态机的几个概念

  1. 状态转换 transition
    目标状态仅由前置状态经过一个事件触发实现状态转换。

  2. 守卫条件 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流程图:
输出的graphviz流程图

输出的mermaid流程图:
输出的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 "";

	}
}

posted @ 2025-11-05 07:38  harrychinese  阅读(32)  评论(0)    收藏  举报