65.9K
CodeProject 正在变化。 阅读更多。
Home

通用状态机和通用组件的尝试

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (6投票s)

2007年7月26日

4分钟阅读

viewsIcon

44068

downloadIcon

398

通用状态机和通用组件的尝试

Screenshot - GenericStateMachine.gif

引言

首先,我必须说明,英语是我的外语,所以请耐心对待我在 Code Project 的第一篇文章。在从事 CTI(计算机电话集成)工作时,我经常使用状态机设计模式。我过去用 C++ 编写过状态机,现在该用 C# 了。Code Project 上已经有一些关于状态机的文章,但似乎没有一个是通用实现的。根据我的经验,状态机的实现如果能做到以下几点,会更有帮助:

  1. 状态机的声明紧凑,这样可以轻松查看所有状态机逻辑,而且不需要图形界面。状态机本身易于理解。
  2. 可以在过渡级别、状态级别和状态机级别接收事件
  3. 状态或事件具有类型信息。枚举或类比整数更好,您不会遇到不可能的事件或状态机进入不可能的状态。
  4. 但又不必限制使用哪种状态或事件类型

背景

为了实现状态和事件的类型信息(3 和 4),我选择用泛型实现状态机。为了实现紧凑的声明(1),我过去在 C++ 中重载了 + 运算符。但在 C# 中,运算符重载非常有限。所以我选择使用 params 关键字,使用可变参数。为了获取通知(2),有委托,所以获取通知很容易。

我的状态机实现包含:

  • 一个或多个状态
  • 起始状态 ID

通过状态机,可以向其发送消息,状态机将对输入消息做出响应。消息包含:

  • 消息 ID。通常这是一个整数或枚举类型。
  • 可选状态。因为消息 ID 通常只是整数或枚举,所以当您需要状态机对同一消息 ID 做出不同响应时,您需要使用状态作为额外信息。
  • 可选参数。参数是可选的,就像状态一样,但状态会引起状态机做出不同响应,而参数则不会。如果您想向状态机传递除消息 ID 之外的更多信息,而又不影响状态机转换,那么这就是您的选择。

状态包含:

  • 状态 ID
  • 一个或多个规则,这些规则将改变状态机的状态

规则包含:

  • 触发规则的消息 ID
  • 可选状态。如果使用,规则仅在触发消息具有相同的 ID 和相同的状态时触发。相同意味着使用 .NET 的 Equals 实现。

使用代码

我只是进行了一个小的测试,还没有在实际应用中使用此代码,因此请自行承担风险。TestStateMachine 项目使用了 NUnit,您需要 NUnit 才能编译它。

好的,我们来看一些代码。首先看看如何使用状态机。

public enum MyState
{
    Off,
    Free,
    Working1,
    Working2
}

public enum MyMessage
{
    PowerOn,
    PowerOff,
    Start,
    Stop,
}

StateMachine m_Machine;
private void button1_Click(object sender, EventArgs e)
{
    State[] states = new State[]
    {
        new State(MyState.Off,
            new Rule(MyMessage.PowerOn, MyState.Free)),
            new State(MyState.Free, new StateHandler(OnEnterStopped), 
                    new StateHandler(OnExitStopped),
            new Rule(MyMessage.PowerOff, MyState.Off),
            new Rule(MyMessage.Start, 1, MyState.Working1),
            new Rule(MyMessage.Start, 2, MyState.Working2, 
                    new RuleHandler(OnWork2))),
        new State(MyState.Working1,
            new Rule(MyMessage.PowerOff, MyState.Off),
            new Rule(MyMessage.Stop, MyState.Free)),
        new State(MyState.Working2,
            new Rule(MyMessage.PowerOff, MyState.Off),
            new Rule(MyMessage.Stop, MyState.Free))
    };

    m_Machine = new StateMachine(MyState.Off, states);
    m_Machine.PutMessage(MyMessage.PowerOn);

    if (MyState.Free != m_Machine.Current)
        MessageBox.Show("wrong");
}

这是一个紧凑的声明。MyState.Off 是初始状态。状态包含状态机的所有状态。如您所见,状态可以有一个或多个规则。在演示中,我展示了规则级别的事件(OnWork2)和状态级别的事件(OnEnterStooped, OnExitStopped)。状态机级别的事件未出现。在演示中,有许多规则处理关机消息,因此您可以在状态机级别处理此消息,并移除所有处理关机消息的规则。这可以通过以下方式完成:

m_Machine.HandleMessage += new MachineHandler(m_Machine_HandleMessage);

处理程序如下所示:

    void m_Machine_HandleMessage(Message message, out bool handled)
    {
        if (message.Id == MyMessage.PowerOff)
        {
            m_Machine.SetState(message, MyState.Off);
            handled = true;
        }
        else
        {
            handled = false;
        }
    }

等等,泛型在哪里?我来给您展示。我希望代码紧凑,并选择了泛型。但不幸的是,泛型通常会使代码冗长。事实上,我添加了这些 using 语句来使代码紧凑。

using StateMachine      = Sogrand.StateMachine.Machine<MyMessage, 
                            MyState, int, int>;
using Message           = Sogrand.StateMachine.Machine<MyMessage, 
                MyState, int, int>.Message;
using Rule              = Sogrand.StateMachine.Machine<MyMessage, 
                            MyState, int, int>.Rule;
using State             = Sogrand.StateMachine.Machine<MyMessage, 
                            MyState, int, int>.State;
using RuleHandler       = Sogrand.StateMachine.Machine<MyMessage, 
                            MyState, int, int>.RuleHandler;
using StateHandler      = Sogrand.StateMachine.Machine<MyMessage, 
                            MyState, int, int>.StateHandler;
using MachineHandler    = Sogrand.StateMachine.Machine<MyMessage, 
                            MyState, int, int>.MachineHandler;

C# 的 using 语句类似于 C/C++ 中的 typedef,但它是通用的(类似于 C++ 模板)。因此,您也需要添加这个 using。但如果您真的在处理状态机,而且您的状态机不是微不足道的;这 7 行额外的代码看起来就不会那么严重了。

通用组件的尝试

我尝试让 VS2005 IDE 编辑状态机。如果您查看代码,您会发现:

public partial class Machine<MessageType, StateType, ParamType>: Component

在测试项目中,我添加了一个组件单元,并进行了如下更改:

public partial class TestMachineEditor : Machine
{
    public TestMachineEditor()
    {
        InitializeComponent();
    }

    public TestMachineEditor(IContainer container)
    {
        container.Add(this);
        InitializeComponent();
    }
}

public class Machine: Sogrand.StateMachine.Machine<MyMessage, 
                            MyState, int, int>
{
}  

VS2005 似乎可以编辑状态机,并生成这些代码:

private void InitializeComponent()
{
    Sogrand.StateMachine.Machine<TestStateMachine.MyMessage,
      TestStateMachine.MyState, int, int>.State state1
       = new Sogrand.StateMachine.Machine<TestStateMachine.MyMessage,
      TestStateMachine.MyState, int, int>.State();

    Sogrand.StateMachine.Machine<TestStateMachine.MyMessage,
      TestStateMachine.MyState, int, int>.Rule rule1
       = new Sogrand.StateMachine.Machine<TestStateMachine.MyMessage,
       TestStateMachine.MyState, int, int>.Rule();

    Sogrand.StateMachine.Machine<TestStateMachine.MyMessage,
      TestStateMachine.MyState, int, int>.State state2
       = new Sogrand.StateMachine.Machine<TestStateMachine.MyMessage,
       TestStateMachine.MyState, int, int>.State();
    //
    // TestMachineEditor
    //
    state1.Id = TestStateMachine.MyState.Off;
    rule1.Id = TestStateMachine.MyMessage.PowerOn;
    rule1.NextState = TestStateMachine.MyState.Free;
    rule1.Param = null;
    state1.Rules = new Sogrand.StateMachine.Machine<TestStateMachine.MyMessage,
     TestStateMachine.MyState, int, int>.Rule[] {rule1};
    state2.Id = TestStateMachine.MyState.Free;
    state2.Rules = new Sogrand.StateMachine.Machine<TestStateMachine.MyMessage,
     TestStateMachine.MyState, int, int>.Rule[0];
    this.States = new Sogrand.StateMachine.Machine<TestStateMachine.MyMessage,
     TestStateMachine.MyState, int, int>.State[] {state1,state2};
}

如果缺少我的 using 语句,生成的代码将非常冗长。但只要 IDE 生成了这些代码,就无关紧要了。但保存并编译后,设计器报告:

类型对象

Sogrand.StateMachine.Machine`3+State
[TestStateMachine.MyMessage,TestStateMachine.MyState,System.Int32][] 

无法转换为类型

Sogrand.StateMachine.Machine`3+State
[TestStateMachine.MyMessage,TestStateMachine.MyState,System.Int32][]

关注点

为了实现泛型状态机,我发现状态、消息、规则必须是状态机类的内部类,以便轻松共享状态类型和消息类型。

历史

  • 2007-7-27:初版
  • 2007-7-31:我发现我混淆了参数和状态。应该是状态影响状态机转换,而参数不应影响。我在状态机定义中添加了 ParamType 以使其更通用。由于通用组件的尝试失败,我更改了状态机基类,它不再是组件。并且移除了状态机、状态和规则的默认构造函数。
这两种类型看起来完全一样。我认为这是 VS2005 IDE 在泛型方面的限制。所以我的通用组件尝试失败了。
© . All rights reserved.