.Net中的状态机库Stateless

一、什么是状态机和状态模式

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

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

二、状态机的要素

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

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

三、Stateless

dotnet-state-machine/stateless at master (github.com)

现在我们来写一个简单的例子:

首先我们定义两个枚举类型用来描述状态和触发

    public enum PhoneState
    {
        OffHook,
        Ringing,
        Connected,
        OnHold,
    }

    public enum PhoneTrigger
    {
        CallDialled,
        CallConnected,
        LeftMessage,
        PlacedOnHold,
        TakenOffHold,
    } 

然后再定义一个类:

    public class IPhone
    {
        //初始化了一个状态机来描述点电话的状态,这里电话的初始状态为挂机状态(OffHook)
        StateMachine<PhoneState, PhoneTrigger> state = new StateMachine<PhoneState, PhoneTrigger>(PhoneState.OffHook);

        public StateMachine<PhoneState, PhoneTrigger> State
        {
            get { return state; }
        }
        public IPhone()
        {
            //当电话处于挂机状态时,如果触发被呼叫事件,电话的状态会变为响铃状态(Ringing)
            state.Configure(PhoneState.OffHook)
                .Permit(PhoneTrigger.CallDialled, PhoneState.Ringing);

            //当电话处于响铃状态时,如果触发通过连接事件,电话的状态会变为已连接状态(Connected)
            state.Configure(PhoneState.Ringing)
                .Permit(PhoneTrigger.CallConnected, PhoneState.Connected);

            state.Configure(PhoneState.Connected)
                .OnEntry(() => StartCallTimer())//当电话处于已连接状态时,系统会开始计时,
                .OnExit(() => StopCallTimer()) //已连接状态变为其他状态时,系统会结束计时
                .Permit(PhoneTrigger.LeftMessage, PhoneState.OffHook)//当电话处于已连接状态时,如果触发留言事件,电话的状态会变为挂机状态(OffHook)
                .Permit(PhoneTrigger.PlacedOnHold, PhoneState.OnHold);//当电话处于已连接状态时,如果触发挂起事件,电话的状态会变为挂起状态(OnHold)
        }

        private void StopCallTimer()
        {
            Console.WriteLine("结束计时");
        }

        private void StartCallTimer()
        {
            Console.WriteLine("开始计时");
        }
    }

再Main函数中使用这个类:

IPhone phone = new IPhone();
            Console.WriteLine($"CurrentState:{phone.State.State}");

            phone.State.Fire(PhoneTrigger.CallDialled);
            Console.WriteLine($"CurrentState:{phone.State.State}");

            phone.State.Fire(PhoneTrigger.CallConnected);
            Console.WriteLine($"CurrentState:{phone.State.State}");

            phone.State.Fire(PhoneTrigger.LeftMessage);
            Console.WriteLine($"CurrentState:{phone.State.State}");
output:

CurrentState:OffHook
CurrentState:Ringing
开始计时
CurrentState:Connected
结束计时
CurrentState:OffHook

分层状态

给状态机对象添加了一组配置。

            //OnHold状态是Connected状态的子状态。这意味着电话挂起的时候,还是连接状态的。
            state.Configure(PhoneState.OnHold)
                .SubstateOf(PhoneState.Connected)
                .Permit(PhoneTrigger.TakenOffHold, PhoneState.Connected);

当电话的状态从已连接(Connected)变为挂起(OnHold)时, 不会触发StartCallTimer()方法和StopCallTimer()方法, 这是因为OnHoldConnected的子状态。

            IPhone phone = new IPhone();
            Console.WriteLine($"CurrentState:{phone.State.State}");

            phone.State.Fire(PhoneTrigger.CallDialled);
            Console.WriteLine($"CurrentState:{phone.State.State}");

            phone.State.Fire(PhoneTrigger.CallConnected);
            Console.WriteLine($"CurrentState:{phone.State.State}");

            phone.State.Fire(PhoneTrigger.PlacedOnHold);
            Console.WriteLine($"CurrentState:{phone.State.State}");

            phone.State.Fire(PhoneTrigger.TakenOffHold);
            Console.WriteLine($"CurrentState:{phone.State.State}");

状态的进入和退出事件

state.Configure(PhoneState.Connected)
                .OnEntry(() => StartCallTimer())//当电话处于已连接状态时,系统会开始计时,
                .OnExit(() => StopCallTimer()) //已连接状态变为其他状态时,系统会结束计时
                .Permit(PhoneTrigger.LeftMessage, PhoneState.OffHook)//当电话处于已连接状态时,如果触发留言事件,电话的状态会变为挂机状态(OffHook)
                .Permit(PhoneTrigger.PlacedOnHold, PhoneState.OnHold);//当电话处于已连接状态时,如果触发挂起事件,电话的状态会变为挂起状态(OnHold)

也可以改为异步方法:

state.Configure(PhoneState.Connected)
                .OnEntryAsync(StartCallTimer)//当电话处于已连接状态时,系统会开始计时,
        private async Task StartCallTimer()
        {
            await Task.Run(() => Console.WriteLine("开始计时"));
        }

但是改为异步配置后,Fire方法也需要使用异步方法。

            phone.State.FireAsync(PhoneTrigger.CallConnected);
            Console.WriteLine($"CurrentState:{phone.State.State}");

还可以指定Trigger:

            state.Configure(PhoneState.Connected)
                .OnEntryFrom(PhoneTrigger.CallConnected, StartCallTimer)//当电话处于已连接状态时,系统会开始计时,

外部状态存储

有时候,当前对象的状态需要来自于一个ORM对象,或者需要将当前对象的状态保存到一个ORM对象中。为了支持这种外部状态存储,StateMachine类的构造函数支持了读写状态值。

var stateMachine = new StateMachine<State, Trigger>(
    () => myState.Value,
    s => myState.Value = s);

内省

状态机可以通过StateMachine.PermittedTriggers属性,提供一个当前对象状态下,可以触发的触发器列表。并提供了一个方法StateMachine.GetInfo()来获取有关状态的配置信息。

保护子句

状态机将根据保护子句在多个转换之间进行选择,配置中的保护子句必须是互斥的,子状态可以通过重新指定来覆盖状态转换,但是子状态不能覆盖父状态允许的状态转换。

PermitIf

参数化触发器

var assignTrigger = stateMachine.SetTriggerParameters<string>(Trigger.Assign);

stateMachine.Configure(State.Assigned)
    .OnEntryFrom(assignTrigger, email => OnAssigned(email));

stateMachine.Fire(assignTrigger, "joe@example.com");

状态改变通知

state.OnTransitioned(trans => Console.WriteLine($"{trans.Trigger}:{trans.Source}=>{trans.Destination}"));

导出DOT图

Stateless还提供了一个在运行时生成DOT图代码的功能,使用生成的DOT图代码,我们可以生成可视化的状态机图。

这里我们可以使用UmlDotGraph.Format()方法来生成DOT图代码。

string graph = UmlDotGraph.Format(state.GetInfo());

然后将字符串在Webgraphviz在线转换成图表。

上一篇:超实用MySQL案例详解!使用docker配置MySQL主从复制


下一篇:您的手机变成计算机的麦克风