.NET中的状态机库Stateless

标题:.NET中的状态机库Stateless 

作者:Lamond Lu 

地址:https://www.cnblogs.com/lwqlun/p/10674018.html[1]

介绍

什么是状态机和状态模式

640?wx_fmt=jpeg

状态机是一种用来进行对象建模的工具,它是一个有向图形,由一组节点和一组相应的转移函数组成。状态机通过响应一系列事件而“运行”。每个事件都在属于“当前” 节点的转移函数的控制范围内,其中函数的范围是节点的一个子集。函数返回“下一个”(也许是同一个)节点。这些节点中至少有一个必须是终态。当到达终态, 状态机停止。

状态模式主要用来解决对象状态转换比较复杂的情况。它把状态的逻辑判断转移到不同的类中,可以把复杂的逻辑简单化。

状态机的要素

状态机有4个要素,即现态、条件、动作、次态。其中,现态和条件是“因”, 动作和次态是“果”。

•现态 - 是指当前对象的状态•条件 - 当一个条件满足时,当前对象会触发一个动作•动作 - 条件满足之后,执行的动作•次态 - 条件满足之后,当前对象的新状态。次态是相对现态而言的,次态一旦触发,就变成了现态

Stateless

Stateless是一款基于.NET的开源状态机库,最新版本4.2.1, 使用它你可以很轻松的在.NET中创建状态机和以状态机为基础的轻量级工作流。

由于整个项目基于.NET Standard的编写的,所以在.NET Framework和.NET Core项目中都可以使用。

项目源代码 https://github.com/dotnet-state-machine/stateless[2]

以下是一个使用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()方法, 这是因为OnHoldConnected的子状态。

外部状态存储

有时候,当前对象的状态需要来自于一个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图例子

640?wx_fmt=png

一个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}"); } }

运行结果

640?wx_fmt=png

下面我们修改代码,我们在创建一个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,有兴趣的同学可以深入研究一下。

References

[1]https://www.cnblogs.com/lwqlun/p/10674018.html
[2]https://github.com/dotnet-state-machine/stateless


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/316380.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[APIO2016] 划艇(dp + 组合数 + 前缀和优化)

problem luogu-P3643 solution 有个显然的暴力 dpdpdp。设 dp(i,j):dp(i,j):dp(i,j): 到了第 iii 个学校&#xff0c;其参加且派出 jjj 个划艇的方案数。 枚举上一个参加的学校以及派出的划艇&#xff0c;则有转移&#xff1a;dp(i,j)∑k<i,j<jdp(k,j′)dp(i,j)\sum_…

.net core webapi 前后端开发分离后的配置和部署

背景&#xff1a;现在越来越多的企业都采用了在开发上前后端分离&#xff0c;前后端开发上的分离有很多种&#xff0c;那么今天&#xff0c;我来分享一下项目中得的前后端分离。B/S Saas 项目&#xff1a;&#xff08;这个项目可以理解成个人中心&#xff0c;当然不止这么点功…

Happy Number

Happy Number 题意 &#xff1a; 开心数是由仅由2&#xff0c;3&#xff0c;6组成的&#xff0c;问第n个开心数是哪个&#xff1f; 1<n<1e9 题解&#xff1a; 正解应该是&#xff1a; 首先确定k大数有几位&#xff1a; 然后就知道k大数是x位下的第k’大的 把2看成0&a…

[HNOI2015] 接水果(倍增 + 整体二分)

problem luogu-P3242 solution 本题的难点在于如何判定路径之间是否覆盖。 这里我们尝试树常见的 dfs\text{dfs}dfs 序。 考虑 x−yx-yx−y 路径如果要覆盖 u−vu-vu−v 路径需要满足怎样的条件。 以下均假设 dfs(u)<dfs(v),dfs(x)<dfs(y)dfs(u)<dfs(v),dfs(x)&…

ASP.NET Core使用Jaeger实现分布式追踪

前言最近我们公司的部分.NET Core的项目接入了Jaeger&#xff0c;也算是稍微完善了一下.NET团队的技术栈。至于为什么选择Jaeger而不是Skywalking&#xff0c;这个问题我只能回答&#xff0c;大佬们说了算。前段时间也在CSharpCorner写过一篇类似的介绍Exploring Distributed T…

[SCOI2007] 修车(费用流 + 差分时间段建图)

problem luogu-P2053 solution 假设只有一个工作人员。修车顺序为 p1,p2,...,pnp_1,p_2,...,p_np1​,p2​,...,pn​ 是一个 nnn 的排列。 那么对于第 iii 个被修的车的等待时间应为 Tp1...TpiT_{p_1}...T_{p_i}Tp1​​...Tpi​​。 总的等待时间则是 Tp1(Tp1Tp2)...(Tp1..…

长沙开发者技术大会暨.NET技术社区成立大会倒数第13天

待你扬帆起航&#xff0c;一起精彩纷呈&#xff01;长沙开发者技术大会暨.NET技术社区成立大会倒数第13天&#xff01;2019年4月21日期待与你相聚在.NET技术社区&#xff01;我们今天会完成海报制作和报表表单&#xff0c;海报内容初步如下所示&#xff1a;活动信息 长沙开发者…

[TJOI2011] 卡片(网络流 + 质因子优化建图)

problem luogu-P2065 solution 这个拿走一组共两张卡片的操作其实就是一个匹配。 直接两个数的最大公约数大于 111 就建一条边&#xff0c;跑二分图匹配最大流即可。 然而如果直接枚举两个数然后算他们的 gcd\text{gcd}gcd &#xff0c;时间复杂度 O(Tn2log⁡V)O(Tn^2\log…

C#并行编程(1):理解并行

什么是并行并行是指两个或者多个事件在同一时刻发生。在程序运行中&#xff0c;并行指多个CPU核心同时执行不同的任务&#xff1b;对于单核心CPU,严格来说是没有程序并行的。并行是为了提高任务执行效率&#xff0c;更快的获取结果。与并发的区别&#xff1a;并发是指两个或者多…

P2163 [SHOI2007]园丁的烦恼(二维数点模板题)

P2163 [SHOI2007]园丁的烦恼 题意&#xff1a; 在一个二维平面内有一些点&#xff0c;给你一个左上角和右下角的点&#xff0c;问这个范围内有多少点 题解&#xff1a; 二维数点模板题 我们设F(a,b)表示以(0,0)为左下角&#xff0c;(a,b)为右上角的矩阵内有多少点 如图不难…

[CQOI2017] 小Q的表格(分块 + 整除分块 + 数学 + 前缀和)

problem luogu-P3700 solution f(a,b)f(b,a)f(a,b)f(b,a)f(a,b)f(b,a) 意味着我们只用考虑半个棋盘的信息。 b∗f(a,ab)(ab)∗f(a,b)b*f(a,ab)(ab)*f(a,b)b∗f(a,ab)(ab)∗f(a,b) 会发现修改 f(a,b)f(a,b)f(a,b) 就影响 f(a,ab)f(a,ab)f(a,ab) 进而影响 f(a,a2b)…f(a,a2b)\…

Orleans MultiClient 多个Silo复合客户端

介绍Orleans.MultiClient 是一个 Orleans 复合客户端&#xff0c;只需要简单配置就可以简单高效连接和请求 Orleans 服务。Orleans.MultiClient 可以轻松连接多个不同服务的 Orleans 服务,在请求 Orleans 时会根据请求的接口自动寻找 Orleans 客户端&#xff0c;使用者无需关心…

[kuangbin]各种各样的题单

[kuangbin]各种各样的题单 专题1 简单搜索 POJ 1321 POJ 2251 POJ 3278 POJ 3279 POJ 1426 POJ 3126 POJ 3087 POJ 3414 FZU 2150 UVA 11624 POJ 3984 HDU 1241 HDU 1495 HDU 2612 专题2 搜索进阶 HDU 1043 HDU 3567 HDU 2181 HDU 3533 HDU 1560 ZOJ 2477 HDU 3085 HDU 1067 HD…

[CQOI2017] 小Q的棋盘(贪心 / 树形dp)

problem luogu-P3698 solution1-贪心 显然我们想尽可能地少走回头路&#xff0c;即一直往下走。 所以我们可以都会有个初步地猜测是走最长链。 但很快就会想到万一这是一条单链&#xff0c;链中的点都是二度点&#xff0c;走得越深回头浪费的步数也越多。 然后就可能直接…

ASP.NET Core 进程内(InProcess)托管(6)《从零开始学ASP.NET CORE MVC》:

本文出自《从零开始学ASP.NET CORE MVC》推荐文章&#xff1a;ASP.NET Core 中的 Main方法ASP.NET Core 进程内(InProcess)托管在这个视频中我们将讨论在ASP.NET Core中的进程内(InProcess)托管模型什么是Kestrel服务器当一个 ASP.NET Core 应用程序执行的时候&#xff0c;.NET…

约会安排 HDU - 4553

约会安排 HDU - 4553 题意&#xff1a; 题意又丑又长就不叙述了 题解&#xff1a; 这个题一开始理解错了。。。题目相当于是有三种情况占据时间&#xff0c;分别是学习&#xff0c;女神和屌丝&#xff0c;我们用不同的lazy来表示女神和屌丝&#xff0c;根据优先级去更新状态…

ML.NET机器学习、API容器化与Azure DevOps实践(一):简介

打算使用几篇文章介绍一下.NET下的机器学习框架ML.NET的具体应用&#xff0c;包括一些常用的业务场景、算法的选择、模型的训练以及RESTful API的创建、机器学习服务容器化&#xff0c;以及基于Azure DevOps的容器化部署等等相关的内容。如果你从来没有玩过机器学习&#xff0c…

[TJOI2011] 书架(线段数优化dp + 单调栈)

problem luogu-P1295 首先可以列出一个暴力 dpdpdp 转移。 设 f(i):f(i):f(i): 到 iii 为止划分若干组&#xff0c;每组最大值的和 的最小值。 然后枚举最后一组&#xff0c;即 iii 所在组的开头 jjj&#xff0c;则 f(i)min⁡{f(j−1)max⁡j≤k≤i{ak}}f(i)\min\Big\{f(j-1…

Picture POJ - 1177(矩形周长并))

Picture POJ - 1177 题目&#xff1a; 多个矩阵相交在一起&#xff0c;问新图形的周长是多少 题解&#xff1a; 参考题解 周长分为两部分&#xff1a;横线和竖线 横线计算方法&#xff1a;现在总区间被覆盖的长度和上一次总区间被覆盖的长度之差的绝对值 那么我们只需要从…

聊一聊C# 8.0中的await foreach

很开心今天能与大家一起聊聊C# 8.0中的新特性-Async Streams,一般人通常看到这个词表情是这样.简单说,其实就是C# 8.0中支持await foreach.或者说,C# 8.0中支持异步返回枚举类型async Task<IEnumerable<T>>.好吧,还不懂?Good,这篇文章就是为你写的,看完这篇文章,你…