首页 > 编程开发 > Objective-C编程 >
-
WF的实际应用 Windows Workflow Fundation应用程序最佳实践
本文使用以下技术:
Microsoft .NET Framework 3.5、Windows Workflow Foundation 和 Visual Studio 2008
目录
工作流编程模型
带副作用的编程
服务,服务,服务
分段执行
单元测试仍是您的好帮手
运行时内的运行时
两极式思维不可取
即插即用
域建模和程序设计
其他建议
Windows Workflow Foundation (WF) 是 Microsoft .NET Framework 3.0 的一部分,自其发布以来,我花费了大量时间研究此技术,使用它实现系统并将相关经验传授给其他人。通过这些经验,我大致总结出一些最佳实践(也有称不上是最佳的),在现实生活中使用 WF 实现软件解决方案。
我曾经遇到过 WF 出现貌似识别危机的问题。与我交谈的许多开发人员,都在一定程度上了解 WF 是怎么回事:条件逻辑、流控制、原子操作等等。
“是的,我想我是了解的。您将一些形状拖动到设计图面上。这些形状代表在某种“流程图”序列中执行的操作。这种方法真是太巧妙了!”之后可能会略表异议“但是,我能用它来做什么呢?”或“但是,我在 C# 或 Visual Basic 中就已经可以执行这样的操作了!”
从理论上说,这些异议都是合理的。虽然从表面上看,WF 就是在 Visual Studio 中的拖放设计器体验,其功能与其他程序的功能看起来也并无大异,但这种外表之下确实存在优点。
要了解 WF 的价值所在,知道一些历史背景将会很有帮助。自 .NET Framework 问世之初,它的主要目标之一就是提高构建 Windows 软件程序的抽象级别。过去,Windows 编程人员需要掌握 COM、HRESULT、智能指针、MTA、消息泵、thunk 层等很多非特定于域的细了内容,任务非常艰巨。
幸好,.NET Framework 的出现已成功解放了编程人员,使他们不必再掌握其中大部分细节内容了。实际上,我们大部分开发人员认为,打开 Visual Studio 并立即开始编写直接映射到手头问题的代码再正常不过了。令人高兴的是,.NET 在运行时会考虑以下问题:内存管理、实施安全策略、代码依赖关系运行时解析和大量的其他详细问题。所以,如今的许多开发人员比以往更有效率。
在通往更高的编程抽象级别的历程中,WF 代表逻辑的下一个步骤。WF 程序在特殊运行时环境中执行,其本身在 .NET CLR 上运行。WF 运行时环境对开发人员编写在该环境中运行的程序施加了一些限制。但是反过来,WF 提供了一组功能强大的、灵活且可扩展的运行时服务,例如支持长时间运行代码,代码执行操作可以持续几天甚至几个月;支持暂停/恢复/取消运行程序;支持审核和跟踪程序执行,甚至支持修改正在运行的程序逻辑!
WF 的核心是其声明性编程模型。也就是说,通常,在 WF 中您可以在 Visual Studio 中拖放 WF 设计器,来描述您希望程序完成的任务。但是,您不能精确指定如何完成此任务,反而是 WF 运行时使用您的程序说明(即控制流和活动的层次图)作为执行计划逻辑的蓝图。
工作流编程模型
在 Visual Studio 中使用工作流设计器 5 分钟,您就会明白 WF 是一种组件技术。WF 中的一个组件就是一个活动。更具体地讲,WF 中的组件是 Activity 基类的任何子类。
活动是 WF 中的操作组合和执行单位。某些活动(这些活动对 CompositeActivity 进行子分类,而 CompositeActivity 本身对 Activity 进行子分类)具有子活动,作为父活动的执行序列的一部分执行。实际上,WF 中的工作流就是这些活动的流程图,从单个根活动开始,然后向任意深度和复杂性扩展。图形中每个活动的原始逻辑,结合此逻辑控制的数据,最终确定活动执行顺序和整体工作流行为。
我喜欢把 WF 活动看成是 Lego 块。您可以将不同颜色、大小和功能的单个 Lego 块(单独每一块的价值很小)合并,构建一个大的整体,其价值超过所有块价值的总和。与此相似,在 WF 中,您将执行离散函数的原子活动合并,以创建高阶逻辑集,然后再将这些逻辑集合并来实现大型业务流程等等。
通常,组件编程(尤其是 WF)常见的一种模式更青睐于组合,而不是继承,这样才可以重复使用。长时间使用 WF 构建日益复杂的工作流,您将会更加认为您正在从小型、自治活动组合程序,而不是创建基于使用 C# 或 Visual Basic 编写的复杂系统的深类层次。
您仍能够在 WF 中将类继承用作重复使用技术。从 SendEmailActivity 派生以创建对象,假设创建的是 SendEncryptedEmailActivity,没有任何问题。在这种情况下,您正在更改执行的操作的基本特征,在此继承技术自然就派上用场。但是,如果需要根据条件仅在特定环境下发送电子邮件,或者在发送电子邮件前后执行其他任务,则最好选择使 SendEmailActivity 成为另一个 CompositeActivity 子类(例如,SequenceActivity 或 IfElseActivity)的子项,然后在 SendEmailActivity 外定义其他逻辑。
图 1 显示了封装有条件的电子邮件操作的复合活动。在该图中,SendEmailActivity 仅负责发送电子邮件,从而提高了它在其他工作流中重复使用的机会。它还允许您重复使用 WF(支持条件分支和顺序活动执行)中现有的结构,而不需要从头开始编写新代码。
图 1 具有条件逻辑的自定义组合活动
带副作用的编程
在编程中,对副作用的定义为:因执行程序的某一部分导致的对共享状态的明显更改。副作用的示例包括更改对象属性的值,或更改数据库表中的一个或多个值等等。在软件中完成实际工作一直用到副作用。
在进行纯函数式编程时,避免共享状态,并且主要通过方法返回值来进行数据流动和操作,与这种编程方法相比,工作流中一切有趣的内容会都带来某种副作用。在工作流程序中,没有返回值等内容,但是,您可以比较执行前和执行后状态来查看更改。您通过使各个活动内外组合,而不是通过调用方法来指示控制流。在设计活动类(作为属性)时活动输入和输出是隐式的,而不是通过方法参数或返回值显式定义的。数据从一个活动到另一个活动的流动,是通过内置支持活动数据绑定完成的。通过与您的自定义 WF 活动逻辑中的配置的服务进行交互,您也可以选择处理共享状态。
WF 活动的基本执行顺序是初始化活动状态、执行活动、检查合成的活动状态是否发生更改,然后对所有其他活动重复上述操作。那么,这恰恰就是活动执行的副作用(“PurchaseOrderTotal 属性的新值是什么?”或“哪些数据库值刚刚进行了更改?”),告知您刚刚发生的情况以及接下来要发生的情况。您可能会使用不适用于其他下游活动的方法使活动处理状态(然而,请注意,此处的风险不会高于 C# 之类的任何命令式语言中所固有的风险),但不必担心数据复制出现问题。
服务,服务,服务
作为一种具有一般目的的编程环境,WF 旨在与外部子系统和代码集成。完成此任务的最直接的方法是,创建自定义工作流活动,来调用数据库、Web 服务或第三方库。在 WF 术语中,这样的外部系统称为服务。
WF 服务的原理很简单:您的工作流中的活动所需的、但又不包含在活动定义中的任何库或功能元素,在 WF 运行时注册为服务:
// create an instance of your service type...
LoggingService logSvc = new LoggingService();
// register it with the WorkflowRuntime instance...
workflowRuntime.AddService( logSvc );
WorkflowInstance instance =
workflowRuntime.CreateWorkflow( typeof( Workflow1 ) );
instance.Start();
在执行期间,活动可要求通过 ActivityExecutionContext(活动从不与主机运行时直接交互)访问所需的服务,并使用配置的服务执行任务,同时使 WF 运行时和主机管理服务生存期:
protected override ActivityExecutionStatus Execute(
ActivityExecutionContext aec ) {
// ActivityExecutionContext provides access to services...
LoggingService logSvc = aec.GetService<LoggingService>();
Debug.Assert( logSvc != null );
logSvc.LogEvent( EvtType, Message );
return ActivityExecutionStatus.Closed;
}
典型服务示例包括记录组件、数据访问层和外部子系统的代理,当然还可能有更多的其他服务。
WF 服务模型的强大功能源于其简单性。简单地说,即任何 .NET 对象都可以配置为服务。请注意:某些其他行为,如服务生存期管理,适用于专为 WorkflowRuntimeService 划分子类的服务。此外,我经常发现通过接口(而不是通过实际键入)公开服务很有用;这将使区分独立于依赖服务的活动的服务实现更加轻松。
因此,此时关键的最佳实践就是从完成此任务所需的构成片段减少自定义 WF 活动的核心逻辑。此外,您应该重构具有状态的服务逻辑,在工作流中多个活动之间共享(例如,记录);在执行期间将引用沿活动图形向下传递给记录组件是件繁琐的事情。这对于在执行周期期间的某个点进行序列化的工作流来说将更加复杂。
接下来,我将介绍其他一些与服务有关的建议。Windows WF 附带一个基础结构(称为本地服务),用于调用活动中的 .NET 类实例并处理活动内部的 .NET 事件。您可以通过 CallExternalMethod 和 HandleExternalEvent 活动与这些本地服务进行交互。他们实质上是位于基本 WF 服务基础结构顶部的附加层,并可以提供与更适合 WF 主机环境(例如 SharePoint)的声明性形式的原始服务相同的好处。但是,对于更多通用的主机,例如 Windows Presentation Foundation (WPF) 或 ASP.NET,此附加层使用起来可能比同等的原始服务配置更加繁琐。请记住,在使用本地服务构建解决方案之前预先考虑承载要求。
此处使用的关键原则是利用 WF 服务模型的功能和灵活性设计分解合理的、可重复使用的工作流活动。
分段执行
使 WF 非常适合通用编程任务的一个功能是它支持异步、无阻塞任务。现代以 Web 服务和以数据库为中心的应用程序的本质是适应异步调用,即发出服务请求后,释放出与该请求调用关联的本地资源以处理其他任务。稍后,完成调度的任务后,执行可在其退出的位置继续进行。ASP.NET 异步页面是此项技术的一个示例。
当 WF 运行时首次执行活动时,即调用其 Execute 方法。活动执行 Execute 中的所有任务后,Execute 将返回 ActivityExecutionStatus.Completed。当 Execute 执行了尽可能多的任务,正在等待某些外部促进因素(例如,对 Web 服务或数据库调用或发送到特定地址的电子邮件的响应)以继续执行操作时,返回 ActivityExecutionStatus.Executing。
图 2 显示了活动的 Execute 方法,用于查询外部服务以获取给定客户 ID 的信用评分。由于信用评分可能需要花费几分钟甚至几小时的时间才能完成,因此工作流将异步调用服务,然后闲置等待结果。在闲置之前,此活动将注册委托以在促进因素到来时由运行时调用。此委托称为延续(字面意思是“指向程序的其余部分的指针”),允许活动创建或定义继续活动进程所需的任何逻辑。单一工作流的生存期可能由几个这样的闲置-继续状态转换构成。
图 2 Execute 方法
protected override ActivityExecutionStatus Execute(
ActivityExecutionContext aec ) {
if ( string.IsNullOrEmpty( SSN ) ) {
throw new InvalidOperationException( "SSN property value is invalid." );
}
ICreditScoring creditSvc = aec.GetService<ICreditScoring>();
Debug.Assert( creditSvc != null );
WorkflowQueuingService queueSvc = aec.GetService<WorkflowQueuingService>();
Debug.Assert( queueSvc != null );
Guid queueName = Guid.NewGuid();
WorkflowQueue queue = queueSvc.CreateWorkflowQueue( queueName, false );
queue.QueueItemAvailable += CreditScoreComputed;
creditSvc.ComputeScoreAsync( SSN, queueName );
return ActivityExecutionStatus.Executing;
}
与工作流实例相关联的工作流队列上的项目(任何 .NET 对象)到达后,运行时会接收到外部促进因素的通知,来唤醒闲置工作流。当异步调用 Web 服务时,等待响应的代码将获取对队列的引用,并将相关数据(推测是最终从 Web 服务接收的任何内容)推入队列。运行时监视队列并调用注册的委托;委托逻辑可获取加入队列的项目并执行任何必要的处理。
此方法的强大功能源于工作流在空闲期间执行的操作。从其 Execute 方法返回 ActivityExecutionStatus.Executing 的活动很显然在某段时期内没有执行任何任务(因此,被视为空闲状态)。可能需要花费几秒钟、几个小时、几天甚至几个月的时间来等待某些外部促进因素。在这段时间,工作流不需要继续保留在内存中消耗计算机资源,而是释放这些资源使其更充分得到利用,因此 WF 运行时支持自动从内存卸载这样的工作流,然后将它们序列化到持久存储,以使用 SqlWorkflowPersistenceService 等持久服务。当外部促进因素到达时,WF 运行时将重新加载工作流并继续正常执行。
单元测试仍是您的好帮手
单元测试始终与致力于 WF 的编程人员的生活息息相关。实际上,WF 的显式组合特性本身适用于编写一组全面的测试,以测试您的解决方案的各个片段是否与其他片段脱离。我将在此处为您提供一些较高级别的信息,但是您一定要访问 2008 年 11 月这一期的《MSDN 杂志》中由 Matt Milner 撰写的 Foundations 专栏文章(“单元测试工作流和活动”),以深入了解单元测试工作流。
对于使用 WF 有一种常见误解,即您只能执行整个工作流(也就是说,进行子分类后的 SequentialWorkflowActivity 实例)。实际上,您可以将任意活动实例作为独立的工作流执行,以便轻松地使用通用活动测试工具执行活动,并验证活动是孤立存在的还是更大的整体中的小子集。图 3 展示的就是这样的测试工具。
图 3 通用工作流测试工具
protected Dictionary<string, object> ExecuteActivity(
Type activityType,
IEnumerable<object>
services,
params object[] inputs ) {
Dictionary<string, object> outputs = null;
Exception ex = null;
using ( WorkflowRuntime workflowRuntime = new WorkflowRuntime() ) {
AutoResetEvent waitHandle = new AutoResetEvent( false );
workflowRuntime.WorkflowCompleted +=
delegate( object sender, WorkflowCompletedEventArgs e ) {
outputs = e.OutputParameters;
waitHandle.Set();
};
workflowRuntime.WorkflowTerminated +=
delegate( object sender, WorkflowTerminatedEventArgs e ) {
ex = e.Exception;
waitHandle.Set();
};
foreach ( object svc in services ) {
workflowRuntime.AddService( svc );
}
Dictionary<string, object> parms = new Dictionary<string, object>();
for ( int i = 0; i < inputs.Length; i += 2 ) {
Debug.Assert( inputs[ i ] is string );
parms.Add( (string) inputs[ i ], inputs[ i + 1 ] );
}
WorkflowInstance instance =
workflowRuntime.CreateWorkflow( activityType, parms );
instance.Start();
waitHandle.WaitOne();
if ( ex != null ) {
Assert.True( false, ex.Message );
return null;
}
Else {
return outputs;
}
}
}
“data in/execute code/make assertions against data”(输入数据/执行代码/根据数据得出结论)这种面向状态的基础测试模式,非常适合于使用通用测试工具对 WF 进行测试。WF 运行时支持自动填充根活动属性值(使用以属性名称作为键的通用 Dictionary)。它还允许您在活动执行完成后,检查根活动属性值。即使您的活动最终将嵌入工作流层次结构内部几个层级,并且活动依靠数据绑定初始化其属性值,您也可以测试此活动是否孤立存在并模拟该活动的全部预期的错误情形。
WF 也非常适合使用更高级的单元测试技术进行测试,如对象模拟,该技术着重于将测试的代码从其依赖关系中隔离,并验证对这些依赖关系的期望值(例如,“执行此测试的代码时,我预期为此依赖项调用 Foo() 方法的次数恰为 3 次”)。下面是一个示例:
// mock the IBanking interface...
var bankingMock = new Mock<IBanking>();
// specify that you expect AddCustomer to
// be called with the customer instance...
bankingMock.Expect( bank => bank.AddCustomer( customer ) );
// execute WF harness, configuring the mock as a service...
ExecuteActivity( typeof( AddCustomerActivity ),
new List<object>() { bankingMock.Object }, "Customer", customer );
如果您熟悉单元测试和对象模拟,您可能已经猜测到 WF 服务是在以 WF 活动为目标的测试中进行模拟的最佳选择。WF 服务本质上是依赖关系,因此不应包括在此类单元测试范围内。
运行时内的运行时
WF 不只是基于组件的可扩展编程环境。它还包含一个执行引擎,构建在 CLR 之上。此引擎提供了许多对工作流程序非常有用的服务,例如,工作计划、在空闲状态时自动序列化/反序列化,以及通过数据绑定将数据从父活动传送到子活动等等。但是您必须谨慎遵守 WF 规则;未试验过的工作流实现在此环境中执行起来可能很差,或者根本无法执行。
也许,要了解 WF 运行时,最重要的一点是了解它对其宿主代码没有任何特定线程要求。相反,它经过专门设计,以便与任何现有线程模型(单线程或多线程)集成。
通过根据 WF 运行时配置的服务计划工作流执行。DefaultWorkflowSchedulerService 在 .NET ThreadPool 上执行工作流,同时 ManualWorkflowSchedulerService 允许您精确指定您要使用的线程(这对亲自将工作分派给 ASP.NET 之类的 ThreadPool 的 WF 主机非常有用)。尽管在实际中很少需要这样做,但您也可以选择提供您自己的自定义计划实现。
幸好,.NET Framework 的出现已成功解放了编程人员,使他们不必再掌握其中大部分细节内容了。实际上,我们大部分开发人员认为,打开 Visual Studio 并立即开始编写直接映射到手头问题的代码再正常不过了。令人高兴的是,.NET 在运行时会考虑以下问题:内存管理、实施安全策略、代码依赖关系运行时解析和大量的其他详细问题。所以,如今的许多开发人员比以往更有效率。
在通往更高的编程抽象级别的历程中,WF 代表逻辑的下一个步骤。WF 程序在特殊运行时环境中执行,其本身在 .NET CLR 上运行。WF 运行时环境对开发人员编写在该环境中运行的程序施加了一些限制。但是反过来,WF 提供了一组功能强大的、灵活且可扩展的运行时服务,例如支持长时间运行代码,代码执行操作可以持续几天甚至几个月;支持暂停/恢复/取消运行程序;支持审核和跟踪程序执行,甚至支持修改正在运行的程序逻辑!
WF 的核心是其声明性编程模型。也就是说,通常,在 WF 中您可以在 Visual Studio 中拖放 WF 设计器,来描述您希望程序完成的任务。但是,您不能精确指定如何完成此任务,反而是 WF 运行时使用您的程序说明(即控制流和活动的层次图)作为执行计划逻辑的蓝图。
工作流编程模型
在 Visual Studio 中使用工作流设计器 5 分钟,您就会明白 WF 是一种组件技术。WF 中的一个组件就是一个活动。更具体地讲,WF 中的组件是 Activity 基类的任何子类。
活动是 WF 中的操作组合和执行单位。某些活动(这些活动对 CompositeActivity 进行子分类,而 CompositeActivity 本身对 Activity 进行子分类)具有子活动,作为父活动的执行序列的一部分执行。实际上,WF 中的工作流就是这些活动的流程图,从单个根活动开始,然后向任意深度和复杂性扩展。图形中每个活动的原始逻辑,结合此逻辑控制的数据,最终确定活动执行顺序和整体工作流行为。