文档章节

.NET中的状态机库Stateless

o
 osc_gu9d45li
发布于 2019/04/09 11:19
字数 2266
阅读 14
收藏 0

行业解决方案、产品招募中!想赚钱就来传!>>>

标题:.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()方法, 这是因为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图例子

一个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,有兴趣的同学可以深入研究一下。

o
粉丝 0
博文 500
码字总数 0
作品 0
私信 提问
加载中
请先登录后再评论。
浏览器中的scheme解释器--SchemeScript

一个用javascript实现的scheme解释器,可以运行在浏览器中或node.js中。 刚刚看到编译原理与实践第二章,一时兴起,想写个以前就想写的scheme的解释器。昨天晚上开始写,到刚才为止,接近一天...

zoowii
2012/11/01
1.1K
0
Android 快速开发框架--ThinkAndroid

ThinkAndroid简介 ThinkAndroid是一个免费的开源的、简易的、遵循Apache2开源协议发布的Android开发框架,其开发宗旨是简单、快速的进行Android应用程序的开发,包含Androidmvc、简易sqlite ...

white-cat
2013/05/05
6.8W
6
在C++中使用Lua

http://www.cppprog.com/2009/0209/62.html

Waiting4you
2009/05/05
1K
0
U3D中如何实现事件回调

unity3d资源插件实现事件回调 Unity3D中所有控制脚本的基类MonoBehaviour有一些虚函数用于绘制中事件的回调,也可以直接理解为事件函数,例如大家都很清楚的Start,Update等函数,以下做个总结...

orientalfashion
2013/05/09
2.5K
1
galera cluster数据备份

可以用标准的数据库备份方法来备份cluster的节点数据。但这样的方法缺少GTID,这种备份也无法把节点恢复到预定的状态。此外,一些备份程序运行时可能会阻断集群操作。联合GTID来备份需要另外...

bupt晶
2015/12/23
846
0

没有更多内容

加载失败,请刷新页面

加载更多

Vim清除最后一个搜索突出显示 - Vim clear last search highlighting

问题: Want to improve this post? 想要改善这篇文章吗? Provide detailed answers to this question, including citations and an explanation of why your answer is correct. 提供此问题......

技术盛宴
17分钟前
11
0
原子属性和非原子属性有什么区别? - What's the difference between the atomic and nonatomic attributes?

问题: What do atomic and nonatomic mean in property declarations? 属性声明中atomic和nonatomic是什么意思? @property(nonatomic, retain) UITextField *userName;@property(atomic, ......

fyin1314
47分钟前
7
0
马化腾每天刷 Leetcode?代码你打算写到几岁?

本文作者:o****0 前几天,一张未证真伪的截图流传,图中显示马化腾几乎每天都会在 Leetcode 上提交代码。 截图还贴出一个 Leetcode 账户地址。该地址的头像已从马化腾的照片换成腾讯 logo,...

百度开发者中心
前天
13
0
滴滴 3000+ Kylin Cube 背后的实践经验揭秘

本次分享主要有三个部分:Kylin 在滴滴的整体应用、架构的实践经验、滴滴全局字典最新版本的实现以及 Kylin 最新实时 OLAP 探索经验分享。 Kylin 在滴滴的应用&架构 Kylin 在滴滴的三类应用场...

浪尖聊大数据
昨天
9
0
ssh“权限太开放”错误 - ssh “permissions are too open” error

问题: I had a problem with my mac where I couldn't save any kind of file on the disk anymore. 我的Mac出现问题,无法再在磁盘上保存任何类型的文件。 I had to reboot OSX lion and r......

javail
今天
19
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部