.NET中的状态机库Stateless
标题:.NET中的状态机库Stateless
作者:Lamond Lu
地址:https://www.cnblogs.com/lwqlun/p/10674018.html
介绍
什么是状态机和状态模式

状态机是一种用来进行对象建模的工具,它是一个有向图形,由一组节点和一组相应的转移函数组成。状态机通过响应一系列事件而“运行”。每个事件都在属于“当前” 节点的转移函数的控制范围内,其中函数的范围是节点的一个子集。函数返回“下一个”(也许是同一个)节点。这些节点中至少有一个必须是终态。当到达终态, 状态机停止。
状态模式主要用来解决对象状态转换比较复杂的情况。它把状态的逻辑判断转移到不同的类中,可以把复杂的逻辑简单化。
状态机的要素
状态机有4个要素,即现态、条件、动作、次态。其中,现态和条件是“因”, 动作和次态是“果”。
- 现态 - 是指当前对象的状态
- 条件 - 当一个条件满足时,当前对象会触发一个动作
- 动作 - 条件满足之后,执行的动作
- 次态 - 条件满足之后,当前对象的新状态。次态是相对现态而言的,次态一旦触发,就变成了现态
Stateless
Stateless是一款基于.NET的开源状态机库,最新版本4.2.1, 使用它你可以很轻松的在.NET中创建状态机和以状态机为基础的轻量级工作流。
由于整个项目基于.NET Standard的编写的,所以在.NET Framework和.NET Core项目中都可以使用。
项目源代码 https://github.com/dotnet-state-machine/stateless
以下是一个使用Stateless编写的打电话流程
var phoneCall = new StateMachine<State, Trigger>(State.OffHook);
phoneCall.Configure(State.OffHook)
.Permit(Trigger.CallDialled, State.Ringing);
phoneCall.Configure(State.Ringing)
.Permit(Trigger.CallConnected, State.Connected);
phoneCall.Configure(State.Connected)
.OnEntry(() => StartCallTimer())
.OnExit(() => StopCallTimer())
.Permit(Trigger.LeftMessage, State.OffHook)
.Permit(Trigger.PlacedOnHold, State.OnHold);
// ...
phoneCall.Fire(Trigger.CallDialled);
Assert.AreEqual(State.Ringing, phoneCall.State);
代码解释
- 当前初始化了一个状态机来描述点电话的状态,这里电话的初始状态为挂机状态(OffHook)
- 当电话处于挂机状态时,如果触发被呼叫事件,电话的状态会变为响铃状态(Ringing)
- 当电话处于响铃状态时,如果触发通过连接事件,电话的状态会变为已连接状态(Connected)
- 当电话处于已连接状态时,系统会开始计时,已连接状态变为其他状态时,系统会结束计时
- 当电话处于已连接状态时,如果触发留言事件,电话的状态会变为挂机状态(OffHook)
- 当电话处于已连接状态时,如果触发挂起事件,电话的状态会变为挂起状态(OnHold)
Fire是触发事件的函数,这里触发了一个呼叫事件- 触发呼叫事件之后,电话的状态变更为响铃状态,所以
Assert.AreEqual(State.Ringing, phoneCall.State)的断言是正确的。
Stateless支持的特性
- 对任何.NET类型的状态和触发器的通用支持
- 分层状态
- 状态的进入和退出事件
- 保护子句以支持条件转换
- 内省
与此同时,还提供一些有用的扩展:
- 支持外部的状态存储(例如:由ORM跟踪属性)
- 参数化触发器
- 可重入状态
- 支持DOT格式图导出
分层状态
在以下例子中,OnHold状态是Connected状态的子状态。这意味着电话挂起的时候,还是连接状态的。
phoneCall.Configure(State.OnHold)
.SubstateOf(State.Connected)
.Permit(Trigger.TakenOffHold, State.Connected)
.Permit(Trigger.PhoneHurledAgainstWall, State.PhoneDestroyed);
状态的进入和退出事件
在前面的例子中,StartCallTimer()方法会在通话连接时执行,StopCallTimer()方法会在通话结束时执行(或者电话挂起的时候,或者把电话被扔到墙上毁坏的时候.)。
当电话的状态从已连接(Connected)变为挂起(OnHold)时, 不会触发StartCallTimer()方法和StopCallTimer()方法, 这是因为OnHold是Connected的子状态。
外部状态存储
有时候,当前对象的状态需要来自于一个ORM对象,或者需要将当前对象的状态保存到一个ORM对象中。为了支持这种外部状态存储,StateMachine类的构造函数支持了读写状态值。
var stateMachine = new StateMachine<State, Trigger>(
() => myState.Value,
s => myState.Value = s);
内省
状态机可以通过StateMachine.PermittedTriggers属性,提供一个当前对象状态下,可以触发的触发器列表。并提供了一个方法StateMachine.GetInfo()来获取有关状态的配置信息。
保护子句
状态机将根据保护子句在多个转换之间进行选择。
phoneCall.Configure(State.OffHook)
.PermitIf(Trigger.CallDialled, State.Ringing, () => IsValidNumber)
.PermitIf(Trigger.CallDialled, State.Beeping, () => !IsValidNumber);
注意:
配置中的保护子句必须是互斥的,子状态可以通过重新指定来覆盖状态转换,但是子状态不能覆盖父状态允许的状态转换。
参数化触发器
Stateless中支持将强类型参数指定给触发器。
var assignTrigger = stateMachine.SetTriggerParameters<string>(Trigger.Assign);
stateMachine.Configure(State.Assigned)
.OnEntryFrom(assignTrigger, email => OnAssigned(email));
stateMachine.Fire(assignTrigger, "joe@example.com");
导出DOT图
Stateless还提供了一个在运行时生成DOT图代码的功能,使用生成的DOT图代码,我们可以生成可视化的状态机图。
这里我们可以使用UmlDotGraph.Format()方法来生成DOT图代码。
phoneCall.Configure(State.OffHook)
.PermitIf(Trigger.CallDialled, State.Ringing, IsValidNumber);
string graph = UmlDotGraph.Format(phoneCall.GetInfo());
生成的DOT图代码例子
digraph {
compound=true;
node [shape=Mrecord]
rankdir="LR"
subgraph clusterOpen
{
label = "Open"
Assigned [label="Assigned|exit / Function"];
}
Deferred [label="Deferred|entry / Function"];
Closed [label="Closed"];
Open -> Assigned [style="solid", label="Assign / Function"];
Assigned -> Assigned [style="solid", label="Assign"];
Assigned -> Closed [style="solid", label="Close"];
Assigned -> Deferred [style="solid", label="Defer"];
Deferred -> Assigned [style="solid", label="Assign / Function"];
}
图形化之后的DOT图例子

一个BugTracker的例子
看完了这么多介绍,下面我们来操练一下, 编写一个Bug的状态机。
假设在当前的BugTracker系统中,Bug有4个种状态Open, Assigned, Deferred, Closed。由此我们可以创建一个枚举类State。
public enum State
{
Open,
Assigned,
Deferred,
Closed
}
如果想改变Bug的状态,这里有3种动作,Assign, Defer, Close。
public enum Trigger
{
Assign,
Defer,
Close
}
下面我们列举一下Bug对象可能的状态变化。
- 每个Bug的初始状态是Open
- 如果当前Bug的状态是Open, 触发动作Assign, Bug的状态会变为Assigned
- 如果当前Bug的状态是Assigned, 触发动作Defer, Bug的状态会变为Deferred
- 如果当前Bug的状态是Assigned, 触发动作Close, Bug的状态会变为Closed
- 如果当前Bug的状态是Assigned, 触发动作Assign, Bug的状态会保持Assigned(变更Bug修改者的场景)
- 如果当前Bug的状态是Deferred, 触发动作Assign, Bug的状态会变为Assigned
由此我们可以编写Bug类
public class Bug
{
State _state = State.Open;
StateMachine<State, Trigger> _machine;
StateMachine<State, Trigger>.TriggerWithParameters<string> _assignTrigger;
string _title;
string _assignee;
public Bug(string title)
{
_title = title;
_machine = new StateMachine<State, Trigger>(() => _state, s => _state = s);
_assignTrigger = _machine.SetTriggerParameters<string>(Trigger.Assign);
_machine.Configure(State.Open).Permit(Trigger.Assign, State.Assigned);
_machine.Configure(State.Assigned)
.OnEntryFrom(_assignTrigger, assignee => _assignee = assignee)
.SubstateOf(State.Open)
.PermitReentry(Trigger.Assign)
.Permit(Trigger.Close, State.Closed)
.Permit(Trigger.Defer, State.Deferred);
_machine.Configure(State.Deferred)
.OnEntry(() => _assignee = null)
.Permit(Trigger.Assign, State.Assigned);
}
public string CurrentState
{
get
{
return _machine.State.ToString();
}
}
public string Title
{
get
{
return _title;
}
}
public string Assignee
{
get
{
if (string.IsNullOrWhiteSpace(_assignee))
{
return "Not Assigned";
}
return _assignee;
}
}
public void Assign(string assignee)
{
_machine.Fire(_assignTrigger, assignee);
}
public void Defer()
{
_machine.Fire(Trigger.Defer);
}
public void Close()
{
_machine.Fire(Trigger.Close);
}
}
代码解释:
- 每个Bug都应该有个指派人和标题,所以这里我添加了一个Assignee和Title属性
- 当指派Bug时,需要指定一个指派人,所以Assign动作的触发器我使用的是一个参数化的触发器
- 当Bug对象进入Assigned状态时,我将当前指定的指派人赋值给了
_assignee字段。
最终效果
这里我们先展示一个正常的操作流程。
class Program
{
static void Main(string[] args)
{
Bug bug = new Bug("Hello World!");
Console.WriteLine($"Current State: {bug.CurrentState}");
bug.Assign("Lamond Lu");
Console.WriteLine($"Current State: {bug.CurrentState}");
Console.WriteLine($"Current Assignee: {bug.Assignee}");
bug.Defer();
Console.WriteLine($"Current State: {bug.CurrentState}");
Console.WriteLine($"Current Assignee: {bug.Assignee}");
bug.Assign("Lu Nan");
Console.WriteLine($"Current State: {bug.CurrentState}");
Console.WriteLine($"Current Assignee: {bug.Assignee}");
bug.Close();
Console.WriteLine($"Current State: {bug.CurrentState}");
}
}
运行结果

下面我们修改代码,我们在创建一个Bug之后,立即尝试关闭它
class Program
{
static void Main(string[] args)
{
Bug bug = new Bug("Hello World!");
bug.Close();
}
}
重新运行程序之后,程序会抛出以下异常。
Unhandled Exception: System.InvalidOperationException: No valid leaving transitions are permitted from state 'Open' for trigger 'Close'. Consider ignoring the trigger.
当Bug处于Open状态的时候,触发Close动作,由于没有任何次态定义,所以抛出了异常,这与我们前面定义的逻辑相符,如果希望程序支持Open -> Closed的状态变化,我们需要修改Open状态的配置,允许Open状态通过Close动作变为Closed状态。
_machine.Configure(State.Open)
.Permit(Trigger.Assign, State.Assigned)
.Permit(Trigger.Close, State.Closed);
由此可见我们完全可以根据自身项目的需求,定义一个简单的工作流,Stateless会自动帮我们验证出错误的流程操作。
总结
今天我为大家分享了一下.NET中的状态机库Stateless, 使用它我们可以很容易的定义出自己业务需要的状态机,或者基于状态机的工作流,本文大部分的内容都来自官方Github,有兴趣的同学可以深入研究一下。
.NET中的状态机库Stateless的更多相关文章
- 对于REST中无状态(stateless)的一点认识(转)
在请求中传递SessionID被普遍认为是unRESTful的,而将用户的credentials包含在每个请求里又是一种非常RESTful的做法.这样一个问题经常会造成困扰.本文就REST的一些概念进 ...
- Boost的状态机库教程(1)
介绍 Boost状态机库一个应用程序框架,你可以用它将UML状态图快速的转换为可执行的c++代码,而不需要任何的代码生成器.它支持几乎所有的UML特征,可以直接了当的转换,并且转换后的c++代码就像对 ...
- 【深入浅出 Yarn 架构与实现】2-4 Yarn 基础库 - 状态机库
当一个服务拥有太多处理逻辑时,会导致代码结构异常的混乱,很难分辨一段逻辑是在哪个阶段发挥作用的. 这时就可以引入状态机模型,帮助代码结构变得清晰. 一.状态机库概述 一)简介 状态机由一组状态组成: ...
- 程序中保存状态的方式之Cookies
程序中保存状态的方式之 Cookies,之前写过一篇关于ViewState的.现在继续总结Cookies方式的 新建的测试页面login <%@ Page Language="C#&q ...
- 程序中保存状态的方式之ViewState
程序中保存状态的方式有以下几种: 1.Application 2.Cookie 3.Session 4.ViewState:ViewState是保存状态的方式之一,ViewState实际就是一个Hid ...
- [DevExpress]GridControl 同步列头checkbox与列中checkbox状态
关键代码: /// <summary> /// 同步列头checkbox与列中checkbox状态 /// </summary> /// <param name=&quo ...
- Android菜鸟的成长笔记(14)—— Android中的状态保存探究(上)
原文:[置顶] Android菜鸟的成长笔记(14)—— Android中的状态保存探究(上) 我们在用手机的时候可能会发现,即使应用被放到后台再返回到前台数据依然保留(比如说我们正在玩游戏,突然电话 ...
- Android菜鸟的成长笔记(15)—— Android中的状态保存探究(下)
原文:Android菜鸟的成长笔记(15)-- Android中的状态保存探究(下) 在上一篇中我们简单了解关于Android中状态保存的过程和原理,这一篇中我们来看一下在系统配置改变的情况下保存数据 ...
- Linux查看系统中socket状态
当我们打开的socket数量很多时,netstat就会变得慢了,有什么办法可以快速查看系统中socket状态? IPv4: $ cat /proc/net/sockstat sockets: used ...
随机推荐
- 【python进阶】深入理解系统进程1
前言 之前程序执⾏都是⼀条腿⾛路,甚⾄是⽤⼀杆枪来打天下. 通过系统编程的学习,会让⼤家有“多条腿”⼀起⾛路,就好⽐有了⼀把机关枪. 此篇为深入理解进程第一篇,下面开始今天的说明~~~ 进程 多任务的 ...
- js实用技巧
IIFE(立即调用函数表达式)最广泛的用途是避免污染全局作用域. 已经有很多JavaScript库和JavaScript高手正在使用这种技巧, 尤其是在最流行jQuery插件的开发者中. 你也应该把这 ...
- 图片滚动(UP)的JS代码详解(offsetTop、scrollTop、offsetHeigh)【转】
源地址 信息技术教材配套的光盘里有一段设置图片滚动的JS代码,与网络上差不多,实现思路:一个设定高度并且隐藏超出它高度的内容的容器demo,里面放demo1和 demo2,demo1是滚动内容,dem ...
- 用Laravel Sms实现 laravel短信验证码的发送
使用Laravel Sms这个扩展包实现短信验证码的发送,这里以阿里云的短信服务为例: 首先,要创建短信签名和短信模板,具体申请详情如下, 接下来,需要创建AccessKey,由于AccessKey是 ...
- Java开源生鲜电商平台-购物车模块的设计与架构(源码可下载)
ava开源生鲜电商平台-购物车模块的设计与架构(源码可下载) 说明:任何一个电商无论是B2C还是B2B都有一个购物车模块,其中最重要的原因就是客户需要的东西放在一起,形成一个购物清单,确认是否有问题, ...
- python_重写数组
class MyArray: '''All the elements in this array must be numbers''' def __IsNumber(self,n): if not i ...
- 微信小程序基于第三方websocket的服务器端部署
微信小程序后台请求越来越严格 1.request要求用https 2.websocket要求用wss 3.测试后发现websocket只能走433端口 作为.net开发,websocket又是使用的第 ...
- elementui左侧菜单栏刷新后还是原来的状态
<template> <div id="leftMenuList"> <el-row> <el-col :span="24&qu ...
- 7-20 jquery遍历节点,bootstrap模态框绑定事件和解绑,mock.js,model.urlroot,id,打基础
7-19 1:$(event.target).parents().filter("tr").find("host-name") 为什么选择不到别的host-na ...
- spring MVC(十)---spring MVC整合mybatis
spring mvc可以通过整合hibernate来实现与数据库的数据交互,也可以通过mybatis来实现,这篇文章是总结一下怎么在springmvc中整合mybatis. 首先mybatis需要用到 ...