(翻译)《WF编程系列之46》 第七章 事件驱动工作流

    在创建一个新的工作流时,需要做出一项重要的抉择:我们要创建的工作流究竟是一个顺序工作流,还是一个状态机工作流?WF提供了两种“即开即用的”(out of the box)工作流执行类型。为了回答这个问题,我们不得不决定谁在受控。

    顺序工作流是一种预知的工作流。执行路径可能是分支、循环、或等待一个外部事件的发生,但是最终顺序工作流将会使用活动、条件和我们在前面章节所提供的必不可少的匹配规则。工作流在进程中受控。

    状态机工作流是一种事件驱动的工作流。就是说,状态机工作流依赖于额外的事件来驱动工作流的完成。我们定义了工作流的合法状态,以及在这些状态之间的合法迁移。工作流总是在其中的一个状态中,并且不得不在迁移到一个新状态之前等待事件的到达。一般来说,重要的选择发生在工作流之外。状态机定义了要遵循的结构,但控制是属于外部世界的。

    当我们能对工作流内部中的大多数决策进行编码时,我们会使用顺序工作流。当决策发生在工作流外部时,我们要使用状态机工作流。在本章中,我们将详细讨论状态机工作流是如何工作的。

7.1 什么是状态机?

    状态机在计算机科学中已经应用了很长一段时间。你将会看到它们在反应系统(reactive system)中尤其流行,就像用于视频游戏和机器人这样的软件。设计者使用状态机为使用状态、事件和迁移的系统建模。

    State,代表一种情形或环境。在下面的截图中,有一个状态机并具备两个状态:Power On状态和Power Off状态。状态机总是这两个状态中的一个。

clip_image002

    事件(event),是一些外部的刺激。在上面的截图中,我们只有一种类型的事件——按钮的点击事件。状态机将会对Power On或Power Off状态上的这个事件做出响应。并不是所有的事件必须对相同的事件做出响应。

    迁移(transition)将状态机转移到下一个状态。迁移只能发生在对事件的响应上。迁移不必将状态机转移到一个新的状态——迁移可以环回(loop back)到相同的状态。当状态机在Power Off状态中接收到按钮的点击事件时,它会迁移到Power On状态。反之,如果状态机在Power On状态中接收到按钮的点击事件,那么它会转移到Power Off状态。

    状态迁移的概念暗示了在迁移之前或之后将会发生一些操作。就是说,状态机并不只是存储状态,它还会在事件到达的时候执行代码。在上面的截图中,状态机在到达一个新的状态时,将会通过打开或关闭电路的方式来控制电流的流向。

7.2 Windows工作流中的状态机

    在上面截图中的状态机是相当简单的,而大多数系统将需要更高级的模型。然而,在截图中介绍的概念(状态、事件和迁移),和我们在Windows工作流中用来创建状态机的概念是相同的。

    在WF中,State活动表示在状态机工作流中的一个状态。随着事件的到达,工作流将会在State活动间迁移。状态机工作流必须指定一个初始状态,这将是该工作流的开始状态。状态机工作流还可以指定一个完成状态(可选的)。工作流将会在其迁移到完成状态后终结。

    EventDriven活动表示状态机中的一个事件。我们把这些活动放在State活动中来表示该状态的合法事件。在EventDriven活动中,我们可以放置一系列将要在事件到达时执行的活动。序列中最后一个活动通常是SetState活动。SetState活动指定了下一个迁移状态。

7.3 我们的第一个状态机

    我们在第2章详细介绍过,我们可以只使用代码来创建工作流,或只使用XAML,或使用代码和XAML(代码分离)。状态机工作流在这一点上并没有区别。我们将在本章使用代码分离的方法创建工作流,虽然其中任何一种创建模式都可以工作。

    我们的工作流将支持Bug跟踪的应用程序。详细而言,随着bug从Open状态迁移到Closed状态,我们将会跟踪软件Bug的生命周期。在生命期内,bug也可以是Assigned、Resolved或Deferred状态。

    为什么要使用状态机来为修复Bug的工作流建模呢?因为对选择bug进行建模是不可能的,而bug是需要到达一个完成状态的。思考一下在bug生命期中的每一步骤所需要的决策。一个新公开的bug需要一些评估。这个bug是重复的么?这个bug真的是一个bug么?即使这个bug真的是一个defect,并不是所有的defect会直接转移到某个人的工作对列中。我们必须针对用来修复这个bug所需要的有效资源和项目计划来评估这个bug的严重程度。如果我们不能把我们所需要的所有智能放入到工作流中,那么我们将依赖于外部的事件来告诉工作流我们做出了什么决策。

7.3.1 创建项目

    就像创建大多数项目那样,我们在Visual Studio的对话框中选择File | New Project。正如在下面的截图中所示,我们将使用State Machine Workflow Console模式的应用程序。项目模板将会创建一个项目,其中带有我们在WF编程中需要引用的所有程序集。

clip_image004

    新的项目将会在名为Workflow1.cs的文件中包括一个默认的工作流。我们可以删除这个文件并添加我们自己的State Machine Workflow(with code separation),命名为BugWorkflow.xoml(参见下面的截图)。

clip_image006

    工作流设计器将和我们新的状态机工作流一起出现(参见下面的截图)。现在,工具箱窗体是可以使用的,其中填充着基础活动库中的活动。然而,最初,我们只能使用活动类型的子集——这些活动类型列出于下面截图的BugFlowInitalState图形中。

clip_image008

    在开始设计我们的状态机之前,我们将需要一些代码支持。特别地,我们需要一个能提供事件的服务来驱动工作流。

7.3.2 Bug的生命期

    状态机将花费大部分时间在等待来自本地通信服务的事件到达上。我们在第3章讨论过本地通信服务,我们需要一个接口来定义服务契约。接口将定义一些事件,服务可以触发这些事件以提供数据到工作流;接口还定义了一些方法,工作流可以在服务上调用这些方法。对于下面这个例子,我们的通讯是单向的——我们仅定义了几个事件。

[ExternalDataExchange]
public interface IBugService
{
    event EventHandler<BugStateChangedEventArgs> BugOpened;
    event EventHandler<BugStateChangedEventArgs> BugResolved;
    event EventHandler<BugStateChangedEventArgs> BugClosed;
    event EventHandler<BugStateChangedEventArgs> BugDeferred;
    event EventHandler<BugStateChangedEventArgs> BugAssigned;
}

    这些事件的事件参数需要进程中的服务传递工作流可以使用的信息。例如,一条可用的信息就是一个携带了bug所有特性(title、description、assignment)的Bug对象。

[Serializable]
public class BugStateChangedEventArgs : ExternalDataEventArgs
{
    public BugStateChangedEventArgs(Guid instanceID, Bug bug):base(instanceID)
    {
        _bug = bug;
        WaitForIdle = true;
    }

    private Bug _bug;

    public Bug Bug
    {
        get { return _bug; }
        set { _bug = value; }
    }
}

    实现了IBugService接口的服务将会在bug的状态发生改变时触发事件。例如,服务可能触发来自Smart Client应用程序的事件以响应用户在UI中操作的bug。有选择地,服务可能在一个web服务的调用中接收到更新过的bug信息,并触发来自ASP.NET的web服务的事件。核心问题是工作流不关心为什么触发,也不关心导致事件的结果。工作流只关心有事件发生了。

    我们将使用bug服务接口的本地实现,并提供触发事件的简单方法。在本章的后面,我们将在控制台模式的程序中使用这个服务以触发事件到工作流。

public class BugService : IBugService
{
    public event EventHandler<BugStateChangedEventArgs> BugOpened;

    public void OpenBug(Guid id, Bug bug)
    {
        if (BugOpened != null)
        {
            BugOpened(null,
                new BugStateChangedEventArgs(id, bug));
        }
    }

    //and so on …
}

    既然我们知道了关于我们的工作流将要使用的服务契约,我们就能够继续创建我们的状态机。


7.3.3 State活动

    State活动代表状态机工作流中的一种状态。不要惊讶,State活动是事件驱动工作流的支柱。通常,我们可以通过拖动工具箱中的State活动到设计器来开始一个工作流设计。如果我们为软件bug的每一个可能的状态拖动一个State活动,我们就具有下面这样的设计器视图:

image

    注意到上面截图中的两种图形,它们在左上角使用了特殊的图标。BugFlowInitialState图形在左上角有一个绿色的图标,因为它是工作流的初始状态。每个状态机工作流必须具有一个初始状态,这将是工作流进入或开始的状态。我们可以通过右击另一个图形并在上下文菜单中选择Set As Initial State来改变初始状态。

    BugClosedState在左上角有一个红色的图标,因为它是完成状态。当一个工作流进入完成状态时,它也就完成了,但是完成状态是可选的。在很多bug跟踪系统中,一个bug可以从关闭(closed)状态重新打开(re-open),但是在我们的工作流中,我们将设置关闭状态为完成状态。我们可以通过右击一个图形并在上下文菜单中选择Set As Completed State来设置完成状态。

    我们的下一步是定义状态机在每一个状态中将要处理的事件。我们将使用EventDriven活动来定义这些事件。

7.3.3.1 EventDriven活动

    EventDriven活动是少数几个我们可以从工具箱中拖出并拖入到State活动中的活动之一。在下面的截图中,我们拖动EventDriven活动到BugFlowInitialState的内部。我们还使用了属性(Properties)窗口来将EventDriven活动的名称修改为OnBugOpened。

image

    OnBugOpened代表了状态机将如何在它的初始状态与BugOpened事件进行交互。我们不能在这一级别的细节上更多地利用该活动。我们需要通过双击OnBugOpened来深入到活动内部。这将带我们进入详细的活动视图中,如下图所示:

image

    这个详细视图沿着设计器的上方显示了一个“面包屑”(breadcrumb)导航控件。面包屑的意图使我们了解到我们正在编辑位于BugFlow工作流中的BugFlowInitialState活动。在这个视图的中间,是我们拖动到状态中的OnBugOpened这个EventDriven活动的详细视图。

    在详细视图中,我们可以看到EventDriven活动就像一个顺序活动,并且它可以保存额外的子活动。然而,这里有一些约束。EventDriven活动中的第一个活动必须实现IEventActivity接口。基础活动库中有三个活动符合这个条件——Delay活动,HandleExternalEvent活动,以及WebServiceInput活动。我们所有的这些事件来自一个本地的通信服务,因此我们将使用HandleExternalEvent活动。

    下面的截图显示了在OnBugOpened活动中的一个HandleExternalEvent活动。我们将活动的名称修改为handleBugOpenedEvent,并将InterfaceType设置为对我们先前定义的IBugService接口的引用。最后,我们选择BugOpened为要处理的事件名称。我们已经完成了所有的初始化工作,我们需要在我们的初始化工作流状态中处理事件。

image

    到目前为止,我们可以在事件处理程序之后继续添加活动。例如,我们可以添加一个活动来向小组成员发送关于这个新bug的通知。当我们完成了添加这些处理活动时,那么最后一个我们想要执行的活动将会是SetState活动,这也是我们接下来将会提及的。

7.3.3.2 SetState活动

    接下来的事件强迫状态机迁移到新的状态。我们可以使用SetState活动为迁移建模,该活动只能出现在状态机工作流的内部。SetState活动是相对简单的。该活动包括了指向目标状态的TargetStateName属性。

    在下面的截图中,我们已经添加了SetState活动到OnBugOpened,并将TargetStateName属性设置为BugOpenState。TargetStateName的属性编辑器在可供选择的下拉列表中只包括有效的状态名称。

image

    我们现在可以点击面包屑中的BugFlow链接,并回过头来查看我们的状态机工作流。设计器将识别出我们刚刚配置的SetState活动,并绘制出一条从BugFlowInitialState图形到BugOpenState的线(参见下面的截图)。工作流设计器为我们展现了一个bug工作流的全景:它开始于BugFlowInitialState,并在接下来的BugOpened事件通知一个新的bug的正式产生时,转移到BugOpenState。

image

    到目前为止,我们可以继续添加EventDriven活动到我们的工作流中。我们需要覆盖在bug生命期中的所有的事件和迁移。状态机的一个优点是,我们控制了哪个事件在哪个具体的状态下是合法的。例如,除了初始状态,我们不想要任何状态来处理BugOpened事件。我们还可以设计我们的状态机,从而在延迟状态中的bug将只会处理一个BugAssigned事件。下面的截图显示了我们的状态机,并在适当的位置具有所有的事件和迁移。

image

    注意到,在上面的截图中,BugClosedState不需要处理任何事件。这个状态是完成状态,并且工作流将不会处理任何额外的事件。

7.3.3.3 StateInitialization和StateFinalization活动

    我们可以拖动到State活动中的两个额外的活动是StateInitialization活动和StateFinalization活动。State活动可以具有一个StateInitialization活动和一个StateFinalization活动。

    这两种活动都将顺次执行一组子活动。当状态机迁移到包括初始化活动的状态时,StateInitialization活动会运行。相反,只要状态机迁移到包括终结活动的状态之外,StateFinalization活动就会执行。使用这两个活动,我们可以在我们的状态机的状态中执行预处理和后事处理。

7.3.4 驱动状态机

    开始一个状态机工作流与开始其它工作流没有什么不同。我们首先创建WorkflowRuntime类的一个实例。我们将需要运行时寄宿(host)一个ExternalDataExchangeService,这就会依次寄宿那些实现了IBugService接口的本地通信服务。第3章包括了本地通信服务,以及ExternalDataExchangeService的更多细节。

ExternalDataExchangeService dataExchange;
dataExchange = new ExternalDataExchangeService();
workflowRuntime.AddService(dataExchange);

BugService bugService = new BugService();
dataExchange.AddService(bugService);

WorkflowInstance instance;
instance = workflowRuntime.CreateWorkflow(
    typeof(BugFlow));

instance.Start();

    在我们的程序中的下一部分代码将调用位于我们的bug服务上的方法。这些方法会触发工作流运行时将要捕获到的事件。我们已经把这些事件小心翼翼地安排到工作流的所有状态中,并成功完成。

Bug bug = new Bug();
bug.Title = "Application crash while printing";

bugService.OpenBug(instance.InstanceId, bug);
bugService.DeferBug(instance.InstanceId, bug);
bugService.AssignBug(instance.InstanceId, bug);
bugService.ResolveBug(instance.InstanceId, bug);
bugService.CloseBug(instance.InstanceId, bug);

waitHandle.WaitOne();

    使用状态机的一个好处是,如果我们的应用程序触发了一个当前工作流状态并不希望触发的事件,那么工作流将触发一个异常。当状态机在它的初始状态时,我们应该只触发BugOpened事件。当状态机在它的Assigned状态时,我们应该只触发BugResolved事件。工作流运行时将保证我们的应用程序遵循状态机所描述的进程。这就提供了一个优势——它保证了编码不正确的应用程序将不会引起状态迁移,这样的工作流被认为不具有可用性,因此将总是遵循工作流编码的企业级处理。然而,需要着重注意的是,任何出发不可用事件的代码将不会引起编译期错误——我们只有到运行期才会看到错误。

    在真实的bug跟踪的应用程序中,一个bug到达关闭状态可能需要数周的时间。幸运的是,状态机工作流可以利用工作流服务,就像跟踪和持久化(全都在第6章描述过)。持久化服务能保存我们的工作流状态,并卸载内存中的实例,然后在事件在数周后到达时重新加载实例。

    关于我们的实例,有一些地方是不寻常的。我们的应用程序在工作流触发每个事件的时候知道工作流的状态。真实的应用程序可能并不知道工作流的这些隐秘信息。我们的应用程序可能并不记得存在了两个月的bug的状态,在这种情形下它也不会知道要触发的合法事件。幸运的是,WF使得这样的信息是可用的。

7.4 检查状态机

    思考一下我们想要为bug跟踪服务提供的用户界面。我们不想给用户创建异常的机会。例如,当bug位于一个并不会转移到关闭状态的状态时,我们不想提供Close This Bug按钮。取而代之的是,我们想要用户界面反映出这个bug的当前状态,并只允许用户执行合法的操作。借助于StateMachineWorkflowInstance类,我们是可以做到这样的。

7.4.1 StateMachineWorkflowInstance

StateMachineWorkflowInstance类为我们提供了接口来管理和查询状态机工作流。正如在下面的类图中所显示的那样,这个API包括了我们可以用来取出当前状态名称的属性,以及找到这个状态的合法迁移。这个类还包括了一个设置状态机状态的方法。虽然我们通常想要bug遵循我们在状态机中设计的工作流,我们可以使用SetState方法把bug设置回它的初始状态,或者强迫这个bug迁移到关闭状态(或者在这中间的任何状态)。

image

    让我们修改一下原始的示例以调用下面的方法。我们将会在调用bug服务的AssignBug方法之后调用这个DumpWorkflow方法,因此工作流应该处于Assigned状态。

private static void DumpStateMachine(WorkflowRuntime runtime, Guid instanceID)
{
    StateMachineWorkflowInstance instance =
        new StateMachineWorkflowInstance(runtime, instanceID);

    Console.WriteLine("Workflow ID: {0}", instanceID);
    Console.WriteLine("Current State: {0}", instance.CurrentStateName);
    Console.WriteLine("Possible Transitions: {0}", instance.PossibleStateTransitions.Count);

    foreach (string name in instance.PossibleStateTransitions)
    {
        Console.WriteLine("\t{0}", name);
    }
}
  这段代码首先使用工作流运行时和工作流ID来检索工作流实例对象。然后我们打印出工作流当前状态的名称、合法迁移的数量,以及合法迁移的名称。输出如下所示:

image 

    我们可以使用上面的信息来自定义用户接口。如果用户在应用程序中打开了这个特别的bug,而这个bug的状态是BugAssignedState,我们最好提供按钮来把这个bug标注为已解决(resolved),或延迟(defer)。这些是当前状态的唯一合法迁移。

    StateMachineWorkflowInstance类的另一个有趣的属性是StateHistory属性。正如你可能猜到的那样,这个属性能够给我们一组工作流所见到的所有状态。如果你还记得我们在第6章关于跟踪服务的讨论,你可能还记得跟踪服务并不是一种彻底的工作来记录工作流的执行历史。如果你猜到StateHistory属性将使用内嵌在WF中的跟踪服务,恭喜你答对了!

7.4.2 状态机跟踪

    第6章提供了我们需要配置、初始化、使用跟踪以及跟踪信息的所有细节,因此我们在这里将不会包括相同的内容。为了使用StateHistory属性,我们必须配置工作流运行时以使用跟踪服务。如果我们试图使用StateHistory属性却没有在适当的位置使用跟踪服务,我们将只能创建一个InvalidOperationException异常。

注意:StateHistory和跟踪服务

到写作本书时为止,如果我们在app.config或web.config中以声明方式配置跟踪服务,那么StateHistory属性将不能工作。取而代之,我们必须以编程方式配置带有连接字符串的跟踪服务,并把该服务传递到工作流运行时中。

    如果我们想要列举出bug经过的状态,可以使用在第6章介绍过的类,如SqlTrackingQuery。我们还可以使用StateMachineWorkflowInstance类和StateHistory属性来为我们完成所有的工作。让我们在关闭bug之前调用下面的方法:

private static void DumpHistory(WorkflowRuntime runtime, Guid instanceID)
{
    StateMachineWorkflowInstance instance =
        new StateMachineWorkflowInstance(runtime, instanceID);

    Console.WriteLine("State History:");

    foreach (string name in instance.StateHistory)
    {
        Console.WriteLine("\t{0}", name);
    }
}

    这段代码给我们如下输出:一组工作流可以看到的状态列表,开始与最近访问过的状态。

image

注意:我们可以在工作流实例仍然运行的时候只使用StateMachineWorkflowInstance类。一旦工作流实例完成,我们必须后退到跟踪服务并使用跟踪服务查询来读取状态机的历史。

7.5 层叠式状态机

    我们的第一个状态机是相对简单的,但它确实代表了便捷状态机的基本设计。然而,有时候,这种直接的方法可能难于管理。想象一下如果用于bug跟踪管理软件的工作流要求我们允许用户关闭或分配(assign)一个bug——而不管bug的当前状态是什么。我们必须为工作流中的assigned和closed事件添加事件驱动活动到每个状态上(已完成的状态除外)。如果我们只需要少数几个状态,那么还好,但是随着状态机的增长,这可能会变得乏味,并易于出错。

    幸运的是,这里有一种比较容易的解决方案。层叠式的状态机允许我们在父状态中内嵌子状态。子状态本质上继承它们父亲的事件驱动活动。如果在我们bug跟踪的工作流中的每个状态需要处理具有相同行为的bug关闭事件,那么我们只需要增加一个事件驱动活动到父状态,并添加我们的bug状态作为这个父亲的后代。

    结果是,状态机工作流本身是StateMachineWorkflowInstance类的一个实例,它派生于StateActivity类(参见下面的截图)。

image

    假设有这样的信息,我们所需要做的全部是为公共事件添加事件驱动活动到我们的工作流中,而不是到每一个状态中。在下面的截图中,我们为来自独立状态的bug的assigned事件和bug的closed事件移除了事件驱动活动,并把它们放在父工作流中。

image

    你将会看到这样的步骤大大降低了我们的状态机的复杂度。实际上,BugDefferedState和BugResolvedState活动在它们中间完全不具有事件驱动活动,它们将继承父亲的行为,并只处理OnBugAssigned和OnBugDeffered事件。所有其它的状态也将继承这些事件驱动活动。层叠式状态机非常适合于企业级异常,如当客户取消了一个订单。如果客户取消了,那么不管当前状态如何,我们都必须停止工作流。

注意:对于层叠式状态机,需要理解的很重要一点是,SetState活动只能把叶子的状态作为目标——就是说不带有任何子状态的状态。

7.6 小结

    在本章中,我们介绍了WF中的状态机。状态机由状态、事件和迁移组成。WF提供了我们需要的所有活动,来为这些各自的组成部分建模。状态机是典型地由本地通信服务、Web服务或工作流运行时服务驱动的,就像跟踪服务和持久化服务一样,状态机和顺序工作流的工作方式是相同的。最后,层叠式状态机支持我们取出公用的事件驱动活动并把它们放到父状态中。

原文地址:https://www.cnblogs.com/Jax/p/1418150.html