一、什么是状态机和状态模式
状态机是一种用来进行对象建模的工具,它是一个有向图像,由一组节点和一组相应的转移函数组成。状态机通过响应一系列事件而“运行”。每一个事件都在属于“当前”节点的转移函数的控制范围内,其中函数的范围是节点的一个子集。函数返回“下一个”(也许是同一个)节点。这些节点中至少由一个必须的终态。当到达终态,状态机停止。
状态模式主要用来解决对象状态转换比较复杂的情况。它把状态的逻辑判断转移到不同的类中,可以把复杂的逻辑简单化。
二、状态机的要素
状态机有四个要素,即现态、条件、动作、次态。其中,现态和条件是“因”,动作和次态是“果”。
- 现态-是值当前对象的状态
- 条件-当一个条件满足时,当前对象会触发一个动作
- 动作-条件满足之后,执行的动作
- 次态-条件满足后,当前对象的新状态。次态是相对现态而言的,次态一旦触发,就变成了现态
三、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()
方法, 这是因为OnHold
是Connected
的子状态。
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在线转换成图表。