配置状态机
介绍 (Introduction)
Complex systems can often be simplified by breaking them down into a series of discrete stages or states with transitions from state to state occurring as the system progresses. A state machine's function is to manage these transitions in response to some sort of input trigger. Ideally, the state machine should know very little about the states and their functionality, it just keeps a reference to the current state and directs all input towards that state. When a transition is made to another state, that state becomes the current state and inputs are directed towards it. The input triggers determine the next selected state, so, if modelling the production of a widget, the quality-control state could transition to the dispatch state or recycling state depending on the trigger received. To illustrate this, have a look at the UML State Machine Diagram of one of the purest forms of the state machine.
复杂的系统通常可以通过将它们分解为一系列离散的阶段或状态来简化,随着系统的发展,状态之间会发生过渡。 状态机的功能是响应某种输入触发来管理这些转换。 理想情况下,状态机应该对状态及其功能了解甚少,它仅保留对当前状态的引用,并将所有输入定向到该状态。 当转换到另一个状态时,该状态变为当前状态,并且输入指向该状态。 输入触发器确定下一个选定的状态,因此,如果对微件的生产建模,则质量控制状态可能会转换为调度状态或回收状态,具体取决于收到的触发器。 为了说明这一点,请查看状态机最纯形式之一的UML状态机图 。
![Image 1](https://www.codeproject.com/KB/cs/5266755/pure.png)
祖先状态机 (The Ancestral State Machine)
This is the basic form of the state machine from which modern versions have evolved. There are only two triggers, a binary 1 or 0. The way to read the diagram is to start at the starting state and follow the transitions from state to state. State A is the starting state, the initial current state. It's marked by having an arrow pointing to it that doesn't come from another state. If this state receives a 0, there is no transition as indicated by the arrow pointing back towards its origin. A 1 causes the machine to make state B the current state. The inputs are now applied to state B where the response is different. A 0 trigger here causes a transition back to state A and a 1 causes a transition to state C. From state C, a 1 goes to state D and a 0 to state A. State D is the end state, it has no transitions to another state. End states are marked by two concentric circles, there can be more than one end state but there is only one start state. So what's the machine doing? It's detecting the pattern 111 within a string of binary inputs. If, at the end of the data stream, the current state is D the input is accepted, if not it's rejected. Notice that there's no memory in the system, the states are unaware of each other and all transitions are triggered by external inputs. The states have no intrinsic functionality; they only act as a target identifier for the triggers.
这是状态机的基本形式,现代版本是从状态机演变而来的。 只有两个触发器,即二进制1或0。读取图表的方法是从起始状态开始,并遵循从一个状态到另一个状态的转换。 状态A是起始状态,即初始当前状态。 它的标志是有一个指向它的箭头,该箭头不是来自其他状态。 如果此状态接收到0,则没有过渡,如指向其起点的箭头所示。 A 1使机器将状态B设为当前状态。 现在,将输入应用于响应不同的状态B。 这里的0触发器将导致返回状态A的转换,而1将导致向状态C的转换。从状态C,a进入状态D,从0到状态A。状态D是结束状态,它没有转换为状态另一种状态。 结束状态由两个同心圆标记,可以有多个结束状态,但只有一个开始状态。 那机器在做什么呢? 它正在检测一串二进制输入中的模式111。 如果在数据流的末尾,当前状态为D,则接受输入,否则,输入被拒绝。 请注意,系统中没有内存,状态彼此之间不知道,并且所有转换都由外部输入触发。 状态没有内在的功能。 它们仅充当触发器的目标标识符。
无状态简介 (A Brief Introduction to Stateless)
The following examples use the popular state machine Stateless that's available for download as a NuGet package. The machine itself is generic, you need to provide the type of the States and the type of the Triggers as well as the starting State when it is initialised. Simple applications usually define an enum
to represent the States and another enum
to represent the Triggers. The enum
s are then used as labels to relate to internally managed pseudo-states and triggers. This arrangement removes the need to define a common interface for all states - a difficult task at the best of times as the whole point of transitioning to a new state is to implement a different functionality.
以下示例使用了流行的状态机Stateless ,该状态机可以作为NuGet软件包下载。 机器本身是通用的,因此在初始化时,您需要提供状态的类型,触发器的类型以及起始状态。 简单的应用程序通常定义一个enum
来表示状态,并定义另一个enum
来表示触发器。 然后将enum
用作与内部管理的伪状态和触发器相关的标签。 这种安排消除了为所有状态定义通用接口的需要-最好的情况下,这是一项艰巨的任务,因为过渡到新状态的关键是实现不同的功能。
使用简单状态机验证电子邮件地址 (Verifying an Email Address Using a Simple State Machine)
This example shows how the basic state machine can be expanded to enable it to verify an email address. The context class is EmailValidator
, it encapsulates the state machine and takes a string inputted from an external source. The string is iterated over and its characters are used as triggers, the string is verified if the machine ends up in an acceptable state when the iteration has finished. The triggers are chars
and the States are an enum
.
此示例显示了如何扩展基本状态机以使其能够验证电子邮件地址。 上下文类是EmailValidator
,它封装状态机并接受从外部源输入的字符串。 迭代字符串,并将其字符用作触发器,如果迭代结束后计算机最终处于可接受的状态,则验证字符串。 触发因素是chars
,状态是enum
。
public enum EmailState
{
Start,
Local,
Domain,
Accepted,
Rejected
}
public class EmailValidator : IValidator
{
private readonly StateMachine<EmailState, char> machine;
public EmailValidator()
{
machine = new StateMachine<EmailState, char>(EmailState.Start);
// ignore unconfigured Trigger exception
machine.OnUnhandledTrigger((state, trigger) => { });
ConfigureMachine();
}
To simplify matters, all illegal characters are filtered out before triggering the machine so the machine can concentrate on determining if an email address is correctly formatted.
为简化起见,在触发机器之前,所有非法字符都会被过滤掉,因此机器可以集中精力确定电子邮件地址的格式是否正确。
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;
}
![Image 2](https://www.codeproject.com/KB/cs/5266755/validate2.png)
The state machine diagram above shows how the machine is 'wired up'. The first character is inputted into the Start state. The characters @, dot and hyphen are rejected; all others are accepted and result in a transition to the Local state that deals with the local part of the email address. The Local state accepts all chars apart from @, @ causes a transition to the Domain state. The first character in the Domain state has to be alphanumeric. The Accepted state accepts everything apart from a hyphen and @.
上面的状态机图显示了如何“连接”机器。 第一个字符输入到开始状态。 字符@,点和连字符被拒绝; 所有其他内容均被接受,并导致过渡到处理电子邮件地址本地部分的本地状态。 本地状态接受@以外的所有字符,@导致转换为域状态。 域状态中的第一个字符必须为字母数字。 接受状态接受除连字符和@之外的所有内容。
配置状态机 (Configuring the State Machine)
States are configured by simply permitting the transitions that are allowed from each state using the Permit
method that takes a trigger and a state as parameters.
通过使用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);
.......
}
状态机图 (State Machine Diagrams)
State Machine diagrams are a great debugging aid as they enable you to visualise the machine's configuration to the extent that someone with no prior knowledge of the configuration can see exactly how the machine is set up. Stateless has a method, UmlDotGraph.Format(machine.GetInfo())
, that outputs a Dot formatted string which, when pasted into the text box at Webgraphviz, will generate a graph. There is a free app available from the site that you can download to do the same thing.
状态机图是一种很好的调试辅助工具,它使您可以可视化机器的配置,以至于对配置没有任何先知的人可以确切地看到机器的设置方式。 Stateless有一个方法UmlDotGraph.Format(machine.GetInfo())
,该方法输出一个Dot格式的字符串,将其粘贴到Webgraphviz的文本框中时,将生成一个图形。 该站点提供了一个免费的应用程序,您可以下载该应用程序以执行相同的操作。
使用状态来处理数据 (Using States to Process Data)
In the previous examples, the states are silent, they don't actually do any work. But, for states to be used in some form of production line, each state needs to be able to carry out work under the direction of the Context
class. The way this is achieved in Stateless is by the use of two Action
delegates, the OnEntry
and OnExit
Action delegates. OnEntry
is called when the state is transitioned into and the OnExit
method is called when the state is transitioned out of. It may be thought that the Action
delegates would not be very useful as they have no parameters and return void
. But that's not the case as the delegates are instantiated in the Context
and so they are able to capture all of the Context's public
and private
variables.
在前面的示例中,状态是静默的,实际上它们没有做任何工作。 但是,要使状态以某种形式在生产线中使用,每个状态都必须能够在Context
类的指导下进行工作。 在无状态状态下,通过使用两个Action
委托( OnEntry
和OnExit
Action委托)来实现此目的。 当状态转换成状态时,调用OnEntry
;当状态转换成状态时, OnExit
方法。 可以认为Action
委托不是很有用,因为它们没有参数并且返回void
。 但是情况并非如此,因为委托是在Context
中实例化的,因此他们能够捕获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);
})
....
An important point is that the Validator
knows nothing about the State or the machine's OnEntry
method that's associated with the State. In order to maintain a good separation of concerns, all transitions should be handled by the Context
. It's not a good idea to allow any helper class such as the Validator
to become trigger happy and start firing on its own volition.
重要的一点是, Validator
对状态或与状态关联的计算机的OnEntry
方法一无所知。 为了保持关注点之间的良好分离,所有过渡都应由Context
处理。 不允许任何帮助器类(例如Validator
变得触发快乐并开始自行发动,这不是一个好主意。
验证示例 (A Validation Example)
![Image 3](https://www.codeproject.com/KB/cs/5266755/Card3.png)
This example mimics the some sort of validation process where the applicant has three chances to input a valid email address. After every unsuccessful attempt, the user has the option to cancel or retry. Three failed attempts result in the application being rejected. The Validating
state is configured to use the trigger Fail
as a guard trigger. Its transition depends upon the bool
returned from the IsRejected
method. If IsRejected
returns true
, the trigger Fail
causes a transition to the Rejected
end state. A false
value causes a transition to the Failed
state where there is an option to cancel or to try again.
此示例模仿某种验证过程,其中申请人有三次输入有效电子邮件地址的机会。 每次尝试失败后,用户可以选择取消或重试。 三次尝试失败均导致该应用程序被拒绝。 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);
Guard triggers are configured using the PermitIf
method. My preference is not to use them and to keep all the logic within the OnEntry Action
rather than let it escape into some sort of guided missile type trigger that hasn't got a predefined target.
使用PermitIf
方法配置保护触发器。 我的首选是不要使用它们并将所有逻辑保留在OnEntry Action
而不是让它逃脱某种没有预定目标的制导导弹类型的触发器。
管理当前状态 (Managing the Current State)
In the validation example, the current state needs to be managed externally to the StateMachine
class so that it can be reset to the Start
state before each attempt at validation. Here's how to set it up, it's just a matter of providing a getter and setter as parameters to the constructor.
在验证示例中,当前状态需要在StateMachine
类的外部进行管理,以便可以在每次尝试验证之前将其重置为Start
状态。 这是设置方法,只需将getter和setter作为参数提供给构造函数即可。
private EmailState currentState= EmailState.Start;
private readonly StateMachine<EmailState, char> 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<EmailState, char>(() => currentState, s => currentState = s);
// ignore unconfigured Trigger exception
machine.OnUnhandledTrigger((state, trigger) => { });
ConfigureMachine();
}
子状态 (Substates)
It's possible to designate a state as being a substate of some other state. The effect of this is that the superstate's OnExit
method is not called when the current state transitions from the superstate to the substate. In this example, SeatBelt
is a substate of Motoring
, Engine
is a substate of SeatBelt
and Brake
is a substate of Engine
. The substate status is inherited so SeatBelt
, Engine
and Brake
are all substates of Motoring
. When the Park
trigger is fired, OnExit
methods will be called consecutively, bubbling up from the Brake
state to the Motoring
state. Typically, there would just be the one external transition permitted into the Motoring
state and its substates but there can be multiple exit triggers.
可以将一个状态指定为某个其他状态的子状态。 这样的效果是,当当前状态从超级状态转换为子状态时,不会调用超级状态的OnExit
方法。 在此示例中, SeatBelt
是Motoring
的子状态, Engine
是SeatBelt
的子状态, Brake
是Engine
的子状态。 子状态的状态是继承的,因此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操作 (Asynchronous OnExitAsync and OnEntryAsync Actions)
The previous example could be refined by keeping the engine running and only stop it after the brakes have been applied. To do this, the Engine
state needs to remain active when it's no longer the current state. So there's a need to run the Engine's OnEntry Action
asynchronously and to end it when the state's OnExit
method is called. The way to achieve this is to use the OnExitAsync Func
and the FireAsync
method.
可以通过保持发动机运转并仅在施加制动后才停止发动机来完善前面的示例。 为此,当Engine
状态不再是当前状态时,它需要保持活动状态。 因此,需要异步运行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");
});
The ChugChug
method is just a bit of nonsense that simulates the engine running.
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);
}
}
When there is a transition from the Brake
state to the Parked
state, the OnExitAsync Func
of the current state is called first, then the call bubbles up through the substates to the Motoring
state. So the OnExitAsync Func
of all the Motoring
states except Engine
need to be configured in a similar way to this.
当存在从一个过渡Brake
状态到Parked
状态下, OnExitAsync Func
的当前状态的首先被调用,则该呼叫经过子状态到冒泡Motoring
状态。 所以OnExitAsync Func
所有的Motoring
除了状态Engine
需要以类似的方式,以这种配置。
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;
});
The StartupAsync
method transitions the current state from the Start
state to the Parked
state.
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);
}
The output from StartupAsync
is mainly frivolous but it gives an idea of how much functionality can be unleashed by simply firing a trigger.
StartupAsync
的输出主要是轻浮的,但是它给出了一个简单的触发触发器就可以释放多少功能的想法。
结论 (Conclusion)
State machines are useful for breaking the code up into a series of discrete sections and progressing the code from section to section. It’s true that configuring the machine needs some care and thought but, once set, it is unlikely to be corrupted by any other user-written code. The ability of state machines to produce a visual representation of the states and their transition triggers is a valuable asset as it provides a ‘wiring diagram’ of how the system is set up and greatly simplifies its maintenance and expansion. A state machine is not a panacea for all multiple pathway scenarios but it certainly knocks the IfThenElse pattern into a cocked hat.
状态机可用于将代码分解为一系列离散的部分,并使代码逐节进行。 确实,配置机器需要一定的注意和考虑,但是一旦设置,它就不会被任何其他用户编写的代码破坏。 状态机产生状态及其过渡触发器的直观表示的能力是一项宝贵的资产,因为它提供了系统设置的“接线图”,并大大简化了其维护和扩展。 状态机并不是所有多路径方案的灵丹妙药,但它肯定会使IfThenElse模式陷入困境。
翻译自: https://www.codeproject.com/Articles/5266755/Configuring-Simple-State-Machines
配置状态机