A .NET 状态机工具包 - 第一部分






4.80/5 (67投票s)
.NET 状态机工具包简介。
目录
引言
状态机一直让我着迷。它们内部运作的精确性在美学上深深吸引着我。它们也是宝贵的编程工具。在构建库和应用程序时,我一次又一次地回到它们。 .NET 状态机工具包的诞生源于我对状态机的兴趣以及我创建它们的少量框架的需求。
这是我 .NET 状态机工具包三篇文章中的第一篇。本文将介绍构成工具包核心的类以及如何创建简单的平面状态机。 第二部分将介绍创建分层状态机以及一些更高级的功能。 第三部分将介绍代码生成以及如何使用 XML 创建状态机。特别感谢 Marc Clifton 建议我将文章分成几部分。我一直在努力,他的建议使事情变得清晰。谢谢你,Marc!
什么是状态机?
状态机是某个对象在响应事件时的行为模型。它通过使其响应与其当前状态相符来模拟行为。状态机响应事件的方式称为转换。转换描述了状态机根据其当前状态接收事件时会发生什么。通常(但并非总是),状态机响应事件的方式是执行某种操作并更改其状态。状态机有时会测试一个条件,以确保其为真,然后再执行转换。这称为守卫。
- 状态机是一种由状态、事件、守卫、动作和转换组成的行为模型。
- 状态是状态机在其生命周期中可以存在的独特条件。
- 事件是发生在状态机上的事情。
- 转换描述了状态机根据其当前状态响应事件时的行为。
- 守卫是状态机在执行转换之前必须为真的条件。
- 动作是状态机在转换期间执行的操作。
对状态机的这种描述快速引入了几个抽象概念,并不意味着是正式或完整的。它只是一个起点。我们将通过示例而不是定义来探索状态机是什么。
电灯开关状态机
我们将看一个非常简单的状态机:电灯开关。它只有两种状态:开和关。当电灯开关处于关闭状态并接收到打开它的事件时,它会转换到打开状态。当电灯开关处于打开状态并接收到关闭它的事件时,它会转换到关闭状态。对于状态机来说,这几乎是最简单的了。

上面的状态图描绘了电灯开关状态机。状态由圆角矩形表示。转换由连接状态的带箭头的曲线表示。箭头表示转换方向,线条上标有触发转换的事件名称。
创建状态机时,它会在其中一个状态中开始生命。这个状态称为初始状态。一个实心圆连接一个带箭头的线指向初始状态。就我们的电灯开关状态机而言,初始状态是关闭状态。
状态图还可以包含其他详细信息。例如,每个转换都可以标有描述状态机在转换期间执行的操作的动作。转换也可以标有守卫。要更深入地了解状态图,请访问 此处。
The .NET State Machine Toolkit
在对状态机进行了简短的介绍之后,我们将开始了解 .NET 状态机工具包。它由少量类组成,如下所述。除了这些类之外,还有许多用于代码生成的类,我将在 第三部分中介绍。我将描述每个类的作用以及关于它们行为的一些重要注意事项。
StateMachine 类
StateMachine 类是所有状态机类的抽象基类。您不应从此类派生您的状态机类,而是从其派生类之一(ActiveStateMachine 类或 PassiveStateMachine 类)派生。当我在本文的其余部分谈论 StateMachine 类时,我指的是 ActiveStateMachine 和 PassiveStateMachine 类共有的功能和行为。
通过使用其 Send 方法将事件发送到 StateMachine。这会将事件及其数据(如果有)放入队列末尾。稍后,StateMachine 将从队列中取出事件并将其分派给其当前 State。此外,还有一个 SendPriority 方法,可将事件放在队列的头部,使其比队列中已有的其他事件先处理。此方法具有受保护的访问权限,因此只有 StateMachine 派生类可以使用它。当 StateMachine 需要将事件发送给自己并需要先于其他事件处理该事件时,它很有用。
当状态机完成触发转换时,它会引发 TransitionCompleted 事件。 TransitionCompletedEventArgs 类伴随该事件。它具有以下属性:
- StateID- 一个整数值,表示当前状态的 ID,即转换的目标状态。在内部转换的情况下,此值不会从上一次转换中更改。
- EventID- 一个整数值,表示触发转换的事件 ID。
- ActionResult- 一个对象,表示与转换关联的操作的结果。此属性本质上允许转换返回值。如果转换的操作没有设置值,此属性可以为- null。
- Error- 一个- Exception对象,表示由转换操作之一抛出的异常。如果没有抛出异常,此属性将为- null。
使用 StateMachine 的客户端可以在将事件发送给 StateMachine 后监听 TransitionCompleted 事件的触发。然后,它可以检查结果并根据需要进行响应。如果操作抛出异常,则会在转换通过 TransitionCompleted 事件完成之后捕获该异常并将其传递。
ActiveStateMachine 类
ActiveStateMachine 类使用 主动对象 设计模式。这意味着 ActiveStateMachine 对象在其自己的线程中运行。在内部,ActiveStateMachine 使用 DelegateQueue 对象来处理和分派事件。当您希望您的状态机成为主动对象时,可以从该类派生您的状态机。
ActiveStateMachine 类实现了 IDisposable 接口。由于它代表一个主动对象,因此需要在某个时候处置它以关闭其线程。我将 Dispose 方法设为 virtual,以便派生的 ActiveStateMachine 类可以覆盖它。通常,派生的 ActiveStateMachine 将覆盖 Dispose 方法,并在调用时使用 SendPriority 方法将一个事件发送给自己,告诉它处置自己。换句话说,处置 ActiveStateMachine 被视为一个事件。您的状态机如何处理处置事件取决于其当前状态。但是,在某个时候,您的状态机需要调用 ActiveStateMachine 的 Dispose(bool disposing) 基类方法,并传递 true 值。这允许基类处置其 DelegateQueue 对象,从而关闭其运行的线程。
PassiveStateMachine 类
与 ActiveStateMachine 类不同,PassiveStateMachine 类不在自己的线程中运行。有时使用主动对象有点过度。在这些情况下,适合从 PassiveStateMachine 类派生您的状态机。
由于 PassiveStateMachine 是被动的,所以必须促使它触发转换。您可以通过调用其 Execute 方法来实现这一点。在向 PassiveStateMachine 派生类发送一个或多个事件后,您会调用 Execute。状态机通过从事件队列中取出所有事件并逐个分派它们来响应。
State 类
State 类代表 StateMachine 在其生命周期中可能处于的状态。一个 State 可以是其他 State 的子状态和/或超状态。
当一个 State 收到事件时,它会检查它是否具有该事件的任何 Transition。如果有,它会遍历该事件的所有 Transition,直到其中一个触发。如果没有找到 Transition,State 会将事件传递给它的超状态(如果存在);该过程会在超状态级别重复。此过程可以无限期地继续,直到 Transition 触发或达到状态层次结构的顶层。
处理完事件后,State 将结果返回到 Dispatch 方法,该方法最初接收了事件。结果指示是否触发了 Transition,如果是,则表示 Transition 的结果 State。它还指示在 Transition 的操作(如果执行了)期间是否发生了异常。状态机使用此信息来更新其当前 State(如果需要)。
SubstateCollection 类
SubstateCollection 类代表子状态的集合。每个 State 都有一个 Substates 属性,其类型为 SubstateCollection。通过此属性将子状态添加到 State 或从 State 中移除。
子状态不以自己的类表示。State 类承担双重职责,在需要时扮演子状态和超状态的角色。一个 State 是否是子状态取决于它是否已添加到另一个 State 的 Substates 集合中。一个 State 是否是超状态取决于是否有任何 State 被添加到其 Substates 集合中。
对于哪些 State 可以作为子状态添加到另一个 State,存在一些限制。最明显的一点是,不能将一个 State 添加到其自身的 Substates 集合中;一个 State 不能成为自身的子状态。此外,一个 State 只能是另一个 State 的直接子状态;您不能将一个 State 添加到多个 State 的 Substates 集合中。
Transition 类
Transition 类代表状态转换。它可以有一个委托来表示它将用于确定是否应该触发的守卫方法。它还可以有一个或多个委托来表示在触发时将执行的操作方法。并且,它可以有一个目标 State,即 Transition 的目标。
TransitionCollection 类
TransitionCollection 代表 Transition 的集合。每个 State 对象都有自己的 TransitionCollection 来保存其 Transition。
当一个 Transition 被添加到 State 的 TransitionCollection 时,它会与一个事件 ID 注册。此事件 ID 是一个标识状态机可接收的事件的值。当状态机接收到事件时,它使用事件的 ID 来检查它是否有该事件的任何 Transition(如上所述)。
实现电灯开关状态机
让我们使用该工具包来构建上面描述的电灯开关状态机。它将有两个状态:on 和 off。以及两个事件:TurnOn 和 TurnOff。首先,我们创建一个派生自 PassiveStateMachine 类的类。
using System;
using Sanford.StateMachineToolkit;
namespace LightSwitchDemo
{
    public class LightSwitch : PassiveStateMachine
    {
        public LightSwitch()
        {
        }
        #region Entry/Exit Methods
        #endregion
        #region Action Methods
        #endregion
    }
}
这是我们的 PassiveStateMachine 派生类的骨架。请注意,我们创建了区域来划分我们将使用的每种方法类型。这纯粹是为了提高代码的可读性。
工具包中事件用整数表示。整数的值充当事件的 ID。用枚举表示事件 ID 最简单。
状态由 State 类表示。每个状态都有自己的 State 对象。此外,每个状态都有自己的 ID。与事件 ID 一样,状态 ID 也是整数值,最好用枚举表示。因此,下一步是创建枚举来表示事件和状态 ID,并添加 State 对象。
using System;
using Sanford.StateMachineToolkit;
namespace LightSwitchDemo
{
    public class LightSwitch : StateMachine
    {
        public enum EventID
        {
            TurnOn,
            TurnOff
        }
        public enum StateID
        {
            On,
            Off
        }
        private State on;
        
        private State off;
// ...
我们将枚举设为公共,以便收听 TransitionCompleted 事件的客户端可以访问事件和状态 ID 值,从而识别与转换关联的事件和状态。
状态机方法
在进一步操作之前,让我们添加状态机的所有方法。
using System;
using Sanford.StateMachineToolkit;
namespace LightSwitchDemo
{       
    public class LightSwitch : StateMachine
    {
        private enum EventID
        {
            TurnOn,
            TurnOff
        }
        public enum StateID
        {
            On,
            Off
        }
        private State on;
        
        private State off;
        private State disposed;
        public LightSwitch()
        {
        }
        #region Entry/Exit Methods
        private void EnterOn()
        {
            Console.WriteLine("Entering On state.");
        }
        private void ExitOn()
        {
            Console.WriteLine("Exiting On state.");
        }
        private void EnterOff()
        {
            Console.WriteLine("Entering Off state.");
        }
        private void ExitOff()
        {
            Console.WriteLine("Exiting Off state.");
        }
        #endregion
        #region Action Methods
        private void TurnOn(object[] args)
        {
            Console.WriteLine("Light switch turned on.");
            ActionResult = "Turned on the light switch.";
        }
        private void TurnOff(object[] args)
        {
            Console.WriteLine("Light switch turned off.");
            ActionResult = "Turned off the light switch.";
        }
        #endregion
    }
}
在以前版本的工具包中,我描述了使用“外观”方法作为简单的包装器来将事件发送到 StateMachine。在此版本的工具包中,我将 StateMachine 类的 Send 方法设为 public 而不是 protected。这意味着外观方法并非严格必需;可以使用 Send 方法直接将事件发送到 StateMachine。但是,如果您愿意,仍然可以编写外观方法;它们有助于隐藏发送事件的一些机制。这是一种风格问题。
进入和退出方法是可选的。进入方法在 State 进入时由 State 调用,退出方法在退出时调用。这里,我们有 on 和 off 状态的进入和退出方法,以及 disposed 状态的进入方法。作为惯例,我们使用 Enter 或 Exit 的名称,后面加上它所属的状态名称。请注意,它们不接受任何参数。另外,请务重要注意,从进入或退出方法抛出异常是非法的,并且会导致未定义行为。
接下来是动作方法。这些方法代表在转换期间执行的操作。请注意,它们将一个对象数组作为唯一的参数。该数组表示传递给 StateMachine 的 Send 方法的参数。事件数据元素的数量可以从零到许多不等。在我们的电灯开关状态机的情况下,事件没有传递其他参数。如果我们的动作方法出了问题,我们可以抛出异常。这是 StateMachine 中唯一可以抛出异常的地方。如前所述,从动作中抛出的任何异常都会被 StateMachine 捕获并通过 TransitionCompleted 事件传递给客户端。
除了上面描述的方法之外,您还可以有守卫方法。这些方法是 Transition 用于确定是否应该触发的方法。我们的电灯开关状态机不需要任何守卫,因此我们没有添加任何。
在离开 StateMachine 的方法之前,让我们看看它们是如何被调用的,以及 StateMachine 通常如何处理事件。
对于 ActiveStateMachine 派生类:
- 通过其 Send方法将事件发送到StateMachine。
- 状态机将其 Dispatch方法以及事件及其任何附带的参数排队到其DelegateQueue。
- 将来某个时候,DelegateQueue将取出Dispatch方法并调用它,并将事件的参数传递给它。
对于 PassiveStateMachine 派生类:
- 通过其 Send方法将事件发送到StateMachine。
- 状态机将事件及其任何附带的参数排队到其事件队列。
- 当调用 Execute方法时,会从队列中取出一个事件。它连同其参数一起传递给Dispatch方法。
此时,被动和主动状态机的步骤相同:
- Dispatch方法将事件分派给状态机的当前- State。
- State检查它是否具有该事件的任何- Transition。如果有,则按照将- Transition添加到- State的顺序进行评估,直到其中一个触发。正是在这个过程中,如果- Transition中添加了守卫方法,就会调用它们。
- 如果 Transition响应事件触发 **并且**Transition有一个目标State,则调用退出方法。可能会退出多个State。这取决于当前State到目标State的路径。
- 如果 Transition有一个动作,此时将执行该动作。
- 调用进入方法。与退出方法一样,可能会调用多个。
- 如果 Transition被触发,则会引发TransitionCompleted事件。
被动状态机将继续分派事件,直到其事件队列为空。
创建状态和转换
接下来,让我们在构造函数中创建 State 对象。
public LightSwitch()
{
    off = new State((int)StateID.Off, 
          new EntryHandler(EnterOff), 
          new ExitHandler(ExitOff));
    on = new State((int)StateID.On, 
         new EntryHandler(EnterOn), 
         new ExitHandler(ExitOn));    
}
每个 State 都以其 ID 初始化。此外,State 还用其进入和退出方法的委托进行初始化。同样,进入和退出方法是可选的。您可以选择不使用它们,在这种情况下,您只需将状态的 ID 传递给 State 的构造函数。
在以前版本的工具包中,State 需要知道它们将接收的事件数量。原因是它们的 TransitionCollection 使用 ArrayList 来存储 Transition。因此,事件 ID 必须是从零到事件总数减一的连续值。并且 TransitionCollection 需要提前知道事件的数量。我发现这是一个脆弱的要求。因此,我已切换为使用哈希表来存储转换。这使您可以自由地为事件 ID 使用任何值。States 无需提前知道事件的数量或其 ID。
现在,让我们设置状态转换,以便当状态机处于 on 状态并接收到 TurnOff 事件时,它会转换到 off 状态;当状态机处于 off 状态并接收到 TurnOn 事件时,它会转换到 on 状态。
public LightSwitch()
{
    off = new State((int)StateID.Off, 3, 
          new EntryHandler(EnterOff), 
          new ExitHandler(ExitOff));
    on = new State((int)StateID.On, 3, 
         new EntryHandler(EnterOn), 
         new ExitHandler(ExitOn)); 
    Transition trans;
    trans = new Transition(on);
    trans.Actions.Add(new ActionHandler(TurnOn));
    off.Transitions.Add((int)EventID.TurnOn, trans);
    trans = new Transition(off);
    trans.Actions.Add(new ActionHandler(TurnOff));
    on.Transitions.Add((int)EventID.TurnOff, trans);
           
    Initialize(off);
}
当我们创建一个 Transition 时,我们向其传递一个表示 Transition 目标的 State 对象。创建 Transition 后,将其添加到 State 的 Transitions 属性中。
在离开构造函数之前,我们使用初始状态初始化了 StateMachine。这是一个重要的步骤,而且很容易忘记。如果您忘记了,当 StateMachine 收到事件时,您将收到一个 InvalidOperationException。我们将 StateMachine 初始化,使其初始处于 off 状态。这在这里的构造函数中完成,但也可以稍后完成。要记住的重要一点是,必须在 StateMachine 收到第一个事件之前完成。
电灯开关演示
现在我们可以编写一个简单的驱动程序来演示我们的 LightSwitch 状态机。
using System;
using System.Threading;
using Sanford.StateMachineToolkit;
namespace LightSwitchDemo
{
    class Class1
    {
        [STAThread]
        static void Main(string[] args)
        {
            LightSwitch ls = new LightSwitch();
            ls.TransitionCompleted += 
                new TransitionCompletedEventHandler(HandleTransitionCompleted);
            ls.Send((int)LightSwitch.EventID.TurnOn);
            ls.Send((int)LightSwitch.EventID.TurnOff);
            ls.Send((int)LightSwitch.EventID.TurnOn);
            ls.Send((int)LightSwitch.EventID.TurnOff);
            ls.Execute();
            Console.Read();
        }
        private static void HandleTransitionCompleted(object sender, 
                TransitionCompletedEventArgs e)
        {
            Console.WriteLine("Transition Completed:");
            Console.WriteLine("\tState ID: {0}", 
               ((LightSwitch.StateID)(e.StateID)).ToString());
            Console.WriteLine("\tEvent ID: {0}", 
              ((LightSwitch.EventID)(e.EventID)).ToString());
            if(e.Error != null)
            {
                Console.WriteLine("\tException: {0}", e.Error.Message);
            }
            else
            {
                Console.WriteLine("\tException: No exception was thrown.");
            }
            if(e.ActionResult != null)
            {
                Console.WriteLine("\tAction Result: {0}", 
                                  e.ActionResult.ToString());
            }
            else
            {
                Console.WriteLine("\tAction Result: No action result.");
            }
        }
    }
}
运行时结果如下:
Entering Off state.
Entering Off state.
Exiting Off state.
Light switch turned on.
Entering On state.
Transition Completed:
        State ID: On
        Event ID: TurnOn
        Exception: No exception was thrown.
        Action Result: Turned on the light switch.
Exiting On state.
Light switch turned off.
Entering Off state.
Transition Completed:
        State ID: Off
        Event ID: TurnOff
        Exception: No exception was thrown.
        Action Result: Turned off the light switch.
Exiting Off state.
Light switch turned on.
Entering On state.
Transition Completed:
        State ID: On
        Event ID: TurnOn
        Exception: No exception was thrown.
        Action Result: Turned on the light switch.
Exiting On state.
Light switch turned off.
Entering Off state.
Transition Completed:
        State ID: Off
        Event ID: TurnOff
        Exception: No exception was thrown.
        Action Result: Turned off the light switch.
请注意,Off 状态首先被进入。这是因为当 StateMachine 用其初始 State 初始化时,它会自动进入。
依赖项
当您下载演示项目(包括工具包的源代码)时,您会发现它无法直接构建。这是因为为了减小 zip 文件的大小,我已经删除了每个项目中的 bin 和 obj 文件夹。这些文件夹包含项目构建所需的程序集。因此,您需要访问我的网站来下载状态机工具包所需的程序集。然后,您需要将它们手动添加到解决方案中的每个项目中。
状态机工具包依赖于我的另外两个命名空间:Sanford.Threading 命名空间和 Sanford.Collections 命名空间。该工具包通过使用其 DelegateQueue 类直接依赖于 Sanford.Threading 命名空间。ActiveStateMachines 使用此类作为事件队列。DelegateQueue 类以前属于工具包本身,但我认为它更适合放在不同的命名空间中;我的其他几个命名空间使用它,而无需工具包的其他功能。
对 Sanford.Collections 命名空间的依赖是间接的。DelegateQueue 类使用了 Sanford.Collections 中的 Deque 类。因此,为了使用 DelegateQueue 类,工具包不仅必须引用 Sanford.Threading 程序集,还必须引用 Sanford.Collections 程序集。您可以在 此处获取这些程序集。
结论
正如我一开始所说,我觉得状态机很有吸引力,也令人着迷。编写这个工具包一直让我非常满意。它不断发展,我希望这个最新版本将是迄今为止最易于使用的版本。如果您觉得这篇文章很有趣,并且觉得这个工具包对您很有用,请看看 第二部分和 第三部分。
保重,并且一如既往,欢迎提出评论和建议。
历史
- 2005年8月29日- 完成第一个版本。
 
- 2005年10月5日- 完成第二个版本,重写文章,并更新源代码。
 
- 2005年10月25日- 完成第三个版本,主要修改文章,并更新源代码。
 
- 2006年3月22日- 完成第四个版本,主要修改文章,并更新源代码。
 
- 2006年5月15日- 完成 4.1 版本,主要修改文章,并更新源代码。
 
- 2006年10月20日- 完成 5.0 版本,主要修改文章,并更新源代码。
 
- 2007年3月29日- 更新了源代码。
 


