原文地址:
https://github.com/dotnet-state-machine/stateless
Stateless
直接在.NET代码中创建状态机和基于轻量级状态机的工作流:
var phoneCall = new StateMachine<State, Trigger>(State.OffHook);
phoneCall.Configure(State.OffHook)
.Permit(Trigger.CallDialled, State.Ringing);
phoneCall.Configure(State.Connected)
.OnEntry(t => StartCallTimer())
.OnExit(t => StopCallTimer())
.InternalTransition(Trigger.MuteMicrophone, t => OnMute())
.InternalTransition(Trigger.UnmuteMicrophone, t => OnUnmute())
.InternalTransition<int>(_setVolumeTrigger, (volume, t) => OnSetVolume(volume))
.Permit(Trigger.LeftMessage, State.OffHook)
.Permit(Trigger.PlacedOnHold, State.OnHold);
// ...
phoneCall.Fire(Trigger.CallDialled);
Assert.AreEqual(State.Ringing, phoneCall.State);
这个项目,以及上面的例子,是受到简单状态机(存档)的启发。
特性
支持大多数标准状态机结构:
- 对任何.NET类型(数字、字符串、枚举等)的状态和触发器的通用支持
- Hierarchical states
- 状态的进入/退出动作
- 保护子句来支持条件转换
- 内省
还提供了一些有用的扩展:
- 能够在外部存储状态(例如,在由ORM跟踪的属性中)
- 参数化的触发器
- 可重入状态
- 导出到DOT图
状态分层
在下面的例子中,OnHold
状态是Connected
状态的子状态。这意味着OnHold
呼叫仍然处于连接状态。
phoneCall.Configure(State.OnHold)
.SubstateOf(State.Connected)
.Permit(Trigger.TakenOffHold, State.Connected)
.Permit(Trigger.PhoneHurledAgainstWall, State.PhoneDestroyed);
除了StateMacheine.State
属性之外,IsInstate(State)
也可以精确的报告当前状态。
IsInStaet(State)
也会考虑子状态,所以如果上文中的例子处于OnHold
状态,IsInState(State.Connected)
的结果也会为true
。
进入/退出动作
在这个例子中,StartCallTimer()
方法将在呼叫连接时执行。StopCallTimer()
将在呼叫完成时执行。
呼叫可以在Connected
和OnHold
状态之间移动,而不需要重复调用StartCallTimer()
和StopCallTimer()
方法,因为OnHold
状态是Connected
状态的子状态。
进入/退出动作处理程序可以使用Transition
类型的参数提供,该参数描述触发器、源和目标状态。
内部转换
有时需要处理触发器,但状态不应该改变。这是一种内部转变。使用InternalTransition
。
初始状态转换
子状态可以标记为初始状态。当状态机进入超级状态时,它也会自动进入子状态。可以这样配置:
sm.Configure(State.B)
.InitialTransition(State.C);
sm.Configure(State.C)
.SubstateOf(State.B);
由于Stateless
的内部结构,它不知道何时启动。这使得无法以传统方式处理初始转换。可以通过添加一个虚拟初始状态来绕过这个限制,然后使用Activate()
来启动状态机。
sm.Configure(InitialState)
.OnActivate(() => sm.Fire(LetsGo))
.Permit(LetsGo, StateA)
外部状态存储器
Stateless
被设计为嵌入到各种应用程序模型中。例如,一些ORM
对可能存储映射数据的位置提出了要求,UI框架通常要求将状态存储在特殊的可绑定属性中。为此,StateMachine
构造函数可以接受用于读写状态值的函数参数:
var stateMachine = new StateMachine<State, Trigger>(
() => myState.Value,
s => myState.Value = s);
在本例中,状态机将使用myState
对象进行状态存储。
另一个示例可以在位于example文件夹中的JsonExample解决方案中找到。
激活/取消激活
在存储对象状态之前可能需要执行一些代码,在恢复对象状态时也是如此。使用Deactivate
和Activate
。激活应该只在正常操作开始之前调用一次,在状态存储之前调用一次。
自省
状态机可以通过StateMachine.PermittedTriggers
属性提供可以在当前状态下成功触发的触发器列表。使用 StateMachine.GetInfo()
检索有关状态配置的信息。
保护子句
状态机将根据保护子句在多个转换之间进行选择,例如:
phoneCall.Configure(State.OffHook)
.PermitIf(Trigger.CallDialled, State.Ringing, () => IsValidNumber)
.PermitIf(Trigger.CallDialled, State.Beeping, () => !IsValidNumber);
在一个状态内的保护子句必须是互斥的(多个保护子句不能同时有效)。子状态可以通过重新指定它们来覆盖转换,但子状态不能禁止超状态允许的转换。
每当触发器被触发时,将评估保护子句。因此,应该使保护子句无副作用。
参数化的触发器
强类型参数可以分配给触发器:
var assignTrigger = stateMachine.SetTriggerParameters<string>(Trigger.Assign);
stateMachine.Configure(State.Assigned)
.OnEntryFrom(assignTrigger, email => OnAssigned(email));
stateMachine.Fire(assignTrigger, "joe@example.com");
触发器参数可用于使用PermitDynamic()
配置方法动态选择目标状态。
忽略转换和可重入状态
触发没有关联允许转换的触发器将导致抛出异常。
要忽略某些状态下的触发器,使用ignore (TTrigger)
指令:
phoneCall.Configure(State.Connected)
.Ignore(Trigger.CallDialled);
或者,一个状态可以被标记为可重入的,这样它的进入和退出动作即使转换是来源或目标是自身时也会触发:
stateMachine.Configure(State.Assigned)
.PermitReentry(Trigger.Assigned)
.OnEntry(() => SendEmailToAssignee());
默认情况下,必须显式忽略触发器。要覆盖Stateless在触发未处理触发器时抛出异常的默认行为,请使用OnUnhandledTrigger
方法配置状态机:
stateMachine.OnUnhandledTrigger((state, trigger) => { });
状态更改通知(事件)
Stateless支持两种类型的状态机事件:
- 状态转换
- 状态转换完成
状态转换
stateMachine.OnTransitioned((transition) => { });
此事件将在每次状态机更改状态时调用。
状态机转换完成
stateMachine.OnTransitionCompleted((transition) => { });
在触发器处理的最后一步,在最后一个进入动作之后调用此事件。
导出到DOT图
在运行时可视化状态机是很有用的。使用这种方法,代码是权威的来源,状态图是总是最新的副产品。
phoneCall.Configure(State.OffHook)
.PermitIf(Trigger.CallDialled, State.Ringing, IsValidNumber);
string graph = UmlDotGraph.Format(phoneCall.GetInfo());
UmlDotGraph.Format()
方法以DOT图形语言的形式返回状态机的字符串表示,例如:
digraph {
OffHook -> Ringing [label="CallDialled [IsValidNumber]"];
}
然后可以通过支持DOT图形语言的工具来呈现,例如来自graphviz.org或viz.js的DOT命令行工具。请参阅http://www.webgraphviz.com获取即时满足。
命令行示例:dot -T pdf -o phoneCall.pdf phoneCall.dot
以生成PDF文件。
异步触发
在提供Task<T>
的平台上,StateMachine
支持async
进入/退出动作等等:
stateMachine.Configure(State.Assigned)
.OnEntryAsync(async () => await SendEmailToAssignee());
在这些情况下,异步处理程序必须使用*Async()
方法注册。
要触发调用异步动作的触发器,必须使用FireAsync()
方法:
await stateMachine.FireAsync(Trigger.Assigned);
注意:虽然StateMachine
可以被_异步的_使用,但它仍然是单线程的,不能被多个线程_同步的_使用。
其他功能
保留SynchronizationContext
在特定的情况下,所有的处理程序方法都必须用消费者的SynchronizationContext
来调用,在创建时设置RetainSynchronizationContext
属性:
var stateMachine = new StateMachine<State, Trigger>(initialState)
{
RetainSynchronizationContext = true
};
例如,在Microsoft Orleans Grain中设置这个是至关重要的,它需要SynchronizationContext
来调用其他Grain。
生成
Stateless 可以在 .NET Framework 4.6.2、.NET Standard 2.0 和 .NET 8.0,在 .NET 运行时版本 4+ 和几乎所有现代 .NET 平台上运行。需要 Visual Studio 2017 或更高版本才能生成解决方案
贡献
我们欢迎对这个项目的贡献。查看CONTRIBUTING.md了解更多信息。
项目目标
本页几乎是对Stateless的完整描述,其明确的目标是保持最小化。
如果您想报告问题或讨论功能,请使用问题跟踪器或讨论页面。
(为什么取这个名字?Stateless实现了一组关于状态转换的规则,但是,至少在使用构造函数的委托版本时,它本身不维护任何内部状态。)