.NET中的状态机库Stateless

发布时间:2019-04-09 11:19  浏览次数:63

标题:.NET中的状态机库Stateless
作者:Lamond Lu
地址:https://www.cnblogs.com/lwqlun/p/10674018.html
<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
<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, "[email protected]");
导出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,有兴趣的同学可以深入研究一下。

标签

归档

阅读排行