目录
介绍
祖先状态机
无状态简介
使用简单状态机验证电子邮件地址
配置状态机
状态机图
使用状态来处理数据
验证示例
管理当前状态
子状态
异步OnExitAsync和OnEntryAsync Action
结论
在说明如何使用状态机进行基本模式识别之后,介绍了一些更高级的实现,例如数据验证,子状态的使用以及在转换为状态和从状态转换时的异步操作方法的实现。
- 下载示例-51.9 KB
复杂的系统通常可以通过将它们分解为一系列离散的阶段或状态来简化,这些状态随系统的进展而发生从状态到状态的转换。状态机的功能是响应某种输入触发来管理这些转换。理想情况下,状态机应该对状态及其功能了解甚少,它仅保留对当前状态的引用,并将所有输入定向到该状态。当转换到另一个状态时,该状态变为当前状态,并且输入指向该状态。输入触发器确定下一个选定的状态,因此,如果对小部件的生产进行建模,质量控制状态可以转换为分派状态或回收状态,这取决于接收到的触发器。为了说明这一点,请看一下UML状态机图,状态机的最纯粹形式之一。
这是状态机的基本形式,现代版本是从其演变而来的。只有两个触发器,即二进制1或0。读取图表的方法是从起始状态开始,并遵循从状态到状态的转换。状态A是起始状态,即初始当前状态。它的标志是有一个指向它的箭头,该箭头不是来自其他状态。如果此状态接收到0,则不会出现指向其起点的箭头所示的过渡。A 1使机器将状态B设为当前状态。现在,将输入应用于响应不同的状态B。此处的0触发器导致返回状态A的转换,而1导致返回状态C的转换。从状态C,1进入状态D, 0到状态A。状态D是结束状态,它没有到另一个状态的转换。结束状态由两个同心圆标记,可以有多个结束状态,但只有一个开始状态。那机器在做什么呢?它正在检测一串二进制输入中的模式111。如果在数据流的末尾,当前状态为D,则表示接受输入,否则,输入被拒绝。请注意,系统中没有内存,状态彼此之间不知道,并且所有转换都由外部输入触发。状态没有内在的功能。它们仅充当触发器的目标标识符。
无状态简介以下示例使用了流行的状态机Stateless,该状态机可作为NuGet软件包下载。机器本身是通用的,因此在初始化时,您需要提供状态的类型和触发器的类型以及起始状态。简单的应用程序通常定义一个enum代表状态,另一个定义enum代表触发器。然后将enum用作与内部管理的伪状态和触发器相关的标签。这种安排消除了为所有状态定义通用接口的需要——最好的时候是一项艰巨的任务,因为过渡到新状态的整个目的是实现不同的功能。
使用简单状态机验证电子邮件地址此示例显示如何扩展基本状态机以使其能够验证电子邮件地址。上下文类为EmailValidator,它封装了状态机并接受从外部源输入的字符串。迭代字符串,并将其字符用作触发器,如果迭代结束后计算机最终处于可接受的状态,则验证字符串。触发器是chars ,状态是enum。
public enum EmailState
{
Start,
Local,
Domain,
Accepted,
Rejected
}
public class EmailValidator : IValidator
{
private readonly StateMachine machine;
public EmailValidator()
{
machine = new StateMachine(EmailState.Start);
// ignore unconfigured Trigger exception
machine.OnUnhandledTrigger((state, trigger) => { });
ConfigureMachine();
}
为简化起见,在触发机器之前,所有非法字符都会被过滤掉,因此机器可以集中精力确定电子邮件地址的格式是否正确。
public bool Validate(string dataString)
{
char[] acceptable = new char[] { '@', '.', '-' };
//rinse out all illegal chars
if (dataString.Any(c => !char.IsLetterOrDigit(c) && !acceptable.Contains(c))
{
return false;
}
foreach (var c in dataString)
{
//use the trigger 'x' for all alphanumeric chars
char trigger = char.IsLetterOrDigit(c) ? 'x' : c;
//The Fire method initiates the state transition.
machine.Fire(trigger);
}
var isValid = machine.IsInState(EmailState.Accepted);
//reset to Start
currentState = EmailState.Start;
return isValid;
}
上面的状态机图显示了如何“连接”机器。第一个字符输入到开始状态。字符@、点和连字符被拒绝;所有其他的都被接受,并导致转换到处理电子邮件地址的本地部分的本地状态。本地状态接受@以外的所有字符,@导致转换为域状态。域状态中的第一个字符必须为字母数字。接受状态接受除连字符和@之外的所有内容。
配置状态机通过使用将Permit触发器和状态作为参数的方法,简单地允许从每个状态允许的转换来配置状态。
private void ConfigureMachine()
{
machine.Configure(EmailState.Start)
.Permit('@', EmailState.Rejected)
.Permit('.', EmailState.Rejected)
.Permit('x', EmailState.Local);
machine.Configure(EmailState.Local)
.Permit('@', EmailState.Domain);
.......
}
状态机图
状态机图是一个很好的调试辅助工具,它使您可以可视化机器的配置,以至于对配置没有任何先知的人可以确切地看到机器的设置方式。Stateless有一种方法,UmlDotGraph.Format(machine.GetInfo()),其可以输出点(Dot)格式的字符串,将其粘贴到Webgraphviz的文本框中时,将生成一个图形。该站点提供了一个免费的应用程序,您可以下载该应用程序以执行相同的操作。
使用状态来处理数据在前面的示例中,状态是静默的,实际上它们没有做任何工作。但是,要在某种形式的生产线中使用状态,每个状态都必须能够在Context类的指导下开展工作。在Stateless中实现此目标的方法是使用两个Action委托,即OnEntry和OnExit Action委托。当状态转换为输入时调用OnEntry,当状态转换为输出时调用OnExit。可以认为,由于Action委托没有参数且返回void,因此委托不是很有用。但是情况并非如此,因为委托是在Context中实例化的,因此他们能够捕获所有上下文中的public和private变量。
private readonly IValidator validator = new EmailValidator();
.....
machine.Configure(State.Validating)
.OnEntry(() =>
{
//prompt for email address
var address= Console.ReadLine();
Trigger trigger = validator.Validate(address) ? Trigger.Accept : Trigger.Fail;
machine.Fire(trigger);
})
....
重要的一点是,Validator不知道与状态相关的状态或机器的OnEntry方法。为了保持关注点之间的良好分离,所有过渡都应由处理Context。让任何辅助类(例如Validator)变得快乐起来并开始自行启动并不是一个好主意。
此示例模仿某种验证过程,其中申请人有三次机会输入有效的电子邮件地址。每次尝试都不成功后,用户可以选择取消或重试。三次尝试失败均导致该应用程序被拒绝。Validating状态配置为将触发器Fail用作保护触发器。其转换取决于IsRejected方法返回的bool值。如果IsRejected返回true,则触发器将Fail导致转换到Rejected结束状态。false值导致转换到Failed状态,其中可以选择取消或重试。
machine.Configure(State.Validating)
.OnEntry(()
{
//prompt for an input
Tweet(Constants.StartValidating);
var address= Console.ReadLine();
Trigger trigger = validator.Validate(address) ? Trigger.Accept : Trigger.Fail;
machine.Fire(trigger);
})
.Permit(Trigger.Accept, State.Accepted)
.PermitIf(Trigger.Fail, State.Failed, () => !IsRejected)
.PermitIf(Trigger.Fail, State.Rejected, () => IsRejected);
使用PermitIf方法配置保护触发器。我的偏好是不使用它们,并保持所有的逻辑在OnEntry Action之内,而不是让它逃逸到某种没有预先设定目标的导弹类型的触发器中。
在验证示例中,当前状态需要在StateMachine类的外部进行管理,以便可以在每次尝试验证之前将其重置为Start状态。设置方法如下,仅需向构造函数提供getter和setter作为参数即可。
private EmailState currentState= EmailState.Start;
private readonly StateMachine machine;
public EmailValidator()
{
//provide a getter and setter so the currentState can be reset to the Start State
//after each attempt at validation
machine = new StateMachine(() => currentState, s => currentState = s);
// ignore unconfigured Trigger exception
machine.OnUnhandledTrigger((state, trigger) => { });
ConfigureMachine();
}
子状态
可以将一个状态指定为某个其他状态的子状态。这样的效果是,当当前状态从超级状态转换为子状态时,不会调用超级状态的OnExit方法。在此示例中,SeatBelt是Motoring的子状态,Engine是Engine的子状态SeatBelt和Brake的子状态。子状态是继承的,所以SeatBelt,Engine和Brake都是Motoring的子状态。当Park触发器被触发,OnExit方法会继续调用,从Brake状态冒泡到Motoring状态。通常,只有一个外部转换允许进入Motoring状态及其子状态,但可以有多个退出触发器。
private void ConfigureMachine()
{
machine.Configure(State.Start)
.Permit(Trigger.Motor, State.Motoring)
.OnEntry(() => Console.WriteLine("In State Start"))
.OnExit(() => Console.WriteLine("Leaving Start"));
machine.Configure(State.Motoring)
.Permit(Trigger.Fasten, State.Seatbelt)
.OnEntry(() => Console.WriteLine("Started Motoring"))
.OnExit(() => Console.WriteLine("Finished Motoring"));
machine.Configure(State.Seatbelt)
.SubstateOf(State.Motoring)
.Permit(Trigger.Engage, State.Engine)
.OnEntry(() => Console.WriteLine("Seatbelt Fastened"))
.OnExit(() => Console.WriteLine("Seatbelt Unfastened"));
machine.Configure(State.Engine)
.SubstateOf(State.Seatbelt)
.Permit(Trigger.Release, State.Brake)
.OnEntry(() => Console.WriteLine("Engine Started"))
.OnExit(() => Console.WriteLine("Engine Off"));
machine.Configure(State.Brake)
.SubstateOf(State.Engine)
.Permit(Trigger.Park, State.Parked)
.OnEntry(() => Console.WriteLine("Brake Released"))
.OnExit(() => Console.WriteLine("Brake Applied"));
machine.Configure(State.Parked)
.OnEntry(() => Console.WriteLine("Parked"));
}
异步OnExitAsync和OnEntryAsync Action
可以通过保持发动机运转并仅在施加制动后才停止发动机来完善前面的示例。为此,当Engine状态不再是当前状态时,该状态需要保持活动状态。因此,需要OnEntry Action异步运行引擎并在调用状态的OnExit方法时结束引擎。实现此目的是使用OnExitAsync Func方法和FireAsync方法。
CancellationTokenSource cts = new CancellationTokenSource();
.....
machine.Configure(State.Engine)
.SubstateOf(State.Seatbelt)
.Permit(Trigger.Release, State.Brake)
.OnEntry( () =>
{
//start the task but don't await it here
engineTask = Task.Run(()=>ChugChug(cts.Token));
Log($"Engine Started {engineNoise}");
})
.OnExitAsync(async() =>
{
cts.Cancel();
await engineTask;
Log("Engine Stopped");
});
ChugChug方法只是一句废话,它可以模拟发动机的运行。
private void ChugChug(CancellationToken token)
{
while (true)
{
//simulate long-running method
Thread.Sleep(5);
//check for cancellation
if (token.IsCancellationRequested) break;
Console.ForegroundColor = ConsoleColor.White;
Console.Write(engineNoise);
}
}
当从Brake状态过渡到Parked状态时,将首先调用当前状态的OnExitAsync Func,然后该调用会通过子状态一直上升到Motoring状态。因此,除了Engine之外的所有Motoring状态的OnExitAsync Func都需要以类似的方式配置。
machine.Configure(State.Brake)
.SubstateOf(State.Engine)
.Permit(Trigger.Park, State.Parked)
.OnEntry(() => Log("Brake Released"))
.OnExitAsync(() =>
{
Log("BreakApplied");
//the method expects a Task to be returned
return Task.CompletedTask;
});
StartupAsync方法将当前状态从Start状态转换为Parked状态。
public async Task StartupAsync()
{
machine.Fire(Trigger.Motor);
machine.Fire(Trigger.Fasten);
machine.Fire(Trigger.Engage);
machine.Fire(Trigger.Release);
string msg = machine.IsInState(State.Motoring) ? "is in " : "is not in ";
Log($"The current state is {machine.State}
it {msg}state Motoring",ConsoleColor.Yellow);
await Task.Delay(50);
Log("\r\nFiring Trigger Park",ConsoleColor.Yellow);
//FireAsync calls the OnExitAsync action of the current state
//The call bubbles up through the substates to State.Motoring
await machine.FireAsync(Trigger.Park);
}
来自StartupAsync的输出主要是琐碎的,但是它给出了一个简单的触发触发器可以释放多少功能的想法。
状态机对于将代码分解为一系列离散的部分并将代码逐节进行是很有用的。确实,配置机器需要一定的注意和考虑,但是一旦设置,它就不会被任何其他用户编写的代码破坏。状态机产生状态及其过渡触发器的可视表示的能力是一项宝贵的资产,因为它提供了系统设置的“接线图”,并大大简化了系统的维护和扩展。状态机并不是所有多路径方案的灵丹妙药,但它肯定会使IfThenElse模式陷入困境。