【翻译】WF从入门到精通(第十二章):策略和规则
学习完本章,你将掌握:
1.知道在工作流处理过程中怎样进行策略和规则的处理
2.理解前向链接以及这是如何影响到基于规则的工作流处理过程的
3.为工作流处理过程创建规则
4.结合Policy活动来使用规则
我敢肯定,我们中的大多数人编写面向过程的代码(imperative code)都很轻松自在。过程式代码指通过编程来实现业务处理过程的C#代码,例如,读取一个数据库表,增加这个表中某些列的值,然后把它们统统都写到另一个数据库的表中。
但在本章,我们将深入规则,规则是对工作流的执行进行控制的一种机制,但它被看作是声明性的(declarative)。通常,声明性代码并不会被编译进程序集中,而是在应用程序执行时被解释。ASP.NET 2.0中有许多新的特征就是声明性的,这其中包括数据绑定和改进了的模板控件。它们能够让你在写ASP.NET应用程序时不使用C#代码就可去执行数据绑定或者其它复杂的控件呈现任务。
Windows Workflow Foundation(WF)也具有声明性的能力,但它是对规则和策略进行绑定而不是数据。你不能使用HTML或者ASP.NET的构造来声明你的规则,当然,涉及的概念都是相似的。
但是什么是规则,什么又是策略呢?
策略和规则
当我写一个涉及到数据或业务过程的程序时,我都会对数据或业务过程进行理解并把它转换成计算机去执行的代码。例如,考虑这样一个对帐目进行检查的处理逻辑:“假如在AvailableBalance列中的值少于要求的值,将抛出一个OverdraftException异常。”这似乎很简单...下面是表达这个行为的一些伪代码:
IF (requestedValue > AvailableBalance) THEN
throw new OverdraftException("Insufficient funds.")
但是要是银行客户具有透支保障功能,假如主账户资金不足时能对次账户进行存取又会怎么样呢?要是客户没有透支保障功能但是可自动设置透支范围的信贷业务又会怎么样呢?要是客户两样都有呢……我们该使用哪一个呢?
就像你能预见到的,为了对各种情况都进行检查,代码就会变得既复杂又混乱。更糟糕的是,它不能很方便地移植到其它业务处理过程中,并且它维护起来可能也很困难。
更进一步,我们看到了这些不只是去进行数据处理而且还有数据之间的关系。在代码中,我们运用过程化的处理方式来对关系进行处理,这些通常都会被翻译成许多嵌套的if语句,swith语句和循环。假如以前你在处理过程中使用了大量的if语句去对所有可能的条件检查,你或许应该问问自己是否已经没有更好的方式了。
至少在WF中有更好的方式。我们可以创建声明性规则然后使用规则引擎(rules engine)来处理它们。声明性规则对关系进行描述说明,它也适合应用到潜在要进行判断的地方。
WF承载了一个规则引擎(rules engine)。该规则引擎可使用XML格式编码的规则,并且能把这些规则应用到你的工作流的方法和字段中。在WF中,你能把面向过程的代码和声明性规则两者结合在一起形成一个总的解决办法。
WF中主要有两个地方会用到规则处理:条件处理和策略。你将发现条件处理是IfElse、While、Replicator以及ConditionedActivityGroup这些活动的一部分。假如你回顾一下第9章“逻辑流活动”和第11章“并行活动”的话,在那些地方介绍和示范的活动中,在每种情况下我都使用一个代码条件来对处理流程进行判断。当然,代码条件的实现是你工作流处理类中的一个事件处理程序(它通过WF所提供的一个CodeCondition类被绑定)。但是,在本章中你将开始使用规则条件进行替换。直到目前在本书中还没有体验过策略的使用,但在本章中当我介绍Policy活动时将对策略进行演示。
备注:对于WF和基于规则的处理可以写完整地一本甚至是一部系列丛书。我不可能在本章覆盖到各个方方面面。但可以做到的是对几个关键的概念进行介绍,这些概念对于你来说是全新的,并且也为你提供了一些基于WF的应用程序,它们用来对基于规则的处理过程的某个特定方面进行演示。假如你对这些话题感兴趣,我强烈建议你花些宝贵时间到Google上(http://www.google.com/),大量的网站都有关于在基于工作流的系统中实现业务处理流程方面的论文和资料。
在WF中,规则(rule)通过条件来表示,它返回一个Boolean值,并伴随着一个或多个操作。WF中规则风格的布局遵循if-then-else风格。规则引擎对条件进行判断,然后指挥工作流按照条件处理的结果去执行。在一定程度上,规则类似于脚本代码,与规则引擎一起充当脚本执行环境。在面向过程的代码之上使用规则的优点是规则能很容易地进行修改,以让你部分的业务处理过程更容易地适应易变的环境。
在WF术语中的策略是指规则的集合,它被包含到一个规则集(RuleSet)中。这使有些被称作前向链接(forward chaining)的事情变得更方便,这个假想的术语指的是在当前处理规则发生改变导致状态变化后,能对规则重新进行判定。
实现规则
规则基于XML,当在Microsoft Visual Studio中生成你的工作流时,这些XML被编译成资源。许多基于WF的类都了解和规则相关的具体细节,它们都在System.Workflow.Activities.Rules中。这些类和XML协同工作去执行规则脚本,最终生成一个以true或者false为结果的条件语句,你的工作流逻辑使用它来指挥处理流程。
在Visual Studio中通过两个主要的用户界面来让规则协同工作。对于简单的规则编辑过程,就像在基于流的活动(在第9章和第11章讨论过)中的条件赋值一样,你可使用一个用户界面来对规则进行编辑,该用户界面能让你生成规则文本。在你的规则中,你可同时使用脚本化的关系运算符(如表12-1所示),算术运算符(如表12-2所示)、逻辑运算符(如表12-3所示)、关键字(如表12-4所示)以及字段、属性和方法,以在你的工作流中为基于流的活动去判定该条件表达式。
表12-1 规则关系运算符
运算符 | 功能 |
==或= | 测试是否相等 |
>或>= | 测试是否大于(>)或者是否大于等于(>=) |
<或<= | 测试是否小于(<)或者是否小于等于(<=) |
表12-2 规则算术运算符
运算符 | 功能 |
+ | 加 |
- | 减 |
* | 乘 |
/ | 除 |
MOD | 模 |
表12-3 规则逻辑运算符
运算符 | 功能 |
AND或&& | 逻辑与 |
OR或|| | 逻辑或 |
NOT或! | 逻辑非 |
& | 位与 |
| | 位或 |
表12-4 规则关键字
运算符 | 功能 |
IF | 开始条件测试 |
THEN | 假如条件测试值为true时所执行的流路径 |
ELSE | 假如条件测试值为false所执行的流路径 |
HALT | 终止规则处理过程,把控制权返回给使用该规则的活动,但是这和使用Terminate活动是不同的。工作流处理过程并没有结束,仅仅是特定的条件停止处理。 |
Update | 通知规则引擎有一个特定的字段或属性值已经被修改了(这可方便地对前向链接进行依赖检查,这将在本章的晚些时候进行论述)。 |
对于策略,你可使用一个专门的编辑器来编辑你的规则集(如图12-1所示)。在这里,你能批量地编辑并组成规则集。你可指定规则的优先级,假如条件改变时怎样对规则进行重新计算,以及指定你想运用的前向链接(forward chaining)机制。当你通过Visual Studio的规则集编辑器用户界面来创建规则时,你可指定规则优先接收的值以及它的前向链接行为。
图12-1 规则集编辑器用户界面
规则属性
当通过规则调用你工作流的方法时,或许会有规则引擎不知道的依赖关系。当它们共享工作流字段或属性时,规则就变成依赖的了。有时依赖关系并不明显,但有时却不。例如,设想一个顾客购买了一定数量的东西后可允许免费送货,但仍然要对手续费进行确定。这可考虑这些规则:
AND
IF this.HandlingCost > 0 THEN
this.OrderCost = this.OrderCost + this.HandlingCost
第一条规则陈述了假如买的东西的数量超过了一个门槛值,就可不用收取运货费用(运货费用的折扣率是100%,注意我们调用了一个方法去设置这个值)。第二条规则在完全不同的工作流部分中执行,它把手续费加到订单价格总额中。假如运费打了折扣,通常也就存在手续费用,但这两条规则是独立的。假如调用了DiscountShipping把一个值写到HandlingCost属性中,并且写入后导致了第二条规则稍后会在处理中去执行(译者注:因为此时handlingCost > 0,会执行第二条规则),你就应当让规则引擎知道这里存在依赖关系,方法是使用一个特殊的基于规则的工作流特性,它们都被列到了表12-5中。下面的代码展示了其中的一个特性行为:
public void DisountShipping(decimal percentageDiscount)
{
// 这里是更新手续费的代码
}
在处理前向链接(forward chaining)时这些特性将起作用。
表12-5 基于规则的特性
特性 | 功能 |
RuleRead | 这个特性是默认的。该特性通知规则引擎方法可读出工作流实例的属性和字段,但不能更新它们的值。 |
RuleWrite | 该特性通知规则引擎工作流方法可更新该潜在依赖的字段或属性的值。 |
RuleInvoke | 该特性告知规则引擎被这个特性修饰的方法可调用一个或多个其它的方法,这些方法或许也会对潜在依赖的字段或属性进行更新。 |
Update语句
表12-4中列出了你可自由使用的基于规则的关键字。它们相对都具有自解释性,但Update例外。作为基于规则的特性,当我们接触前向链接时将对Update进行更多的讨论,但核心思想是要通知规则引擎你的规则是在明确地对一个字段或属性进行更新,以便使其它相依赖的规则知道这个更改。Update实际上并不对字段或属性进行修改——它只是通知规则引擎该字段或属性被改变了。
Update需要一个单一的字符串,它表示字段或属性的名称,它的用途是通知规则引擎相依赖的规则可能需要重新判定。尽管最佳做法的原则是使用基于规则的特性作为首选,但有时使用Update更恰当。这有一个很好的例子:当你在一个工作流程序集上不能进行写入操作但要修改某个属性时(一是没有基于规则的属性,一是你不能更新源代码以让它包含必要的特性)。
可能对于理解在工作流处理过程中能怎样去使用规则的最好方式是开始去写一些代码进行试验。我们将从规则条件开始,它可和我们在第9章中使用过的代码条件相对比。
规则条件
对条件表达式进行判定的WF活动有IfElse活动、While活动、Replicator活动和ConditionedActivityGroup活动。这些活动当中的每一个都会要求你做一个true/false的判断。在第9章中,我们使用过“代码条件”属性设置,它使Visual Studio向我们的工作流代码中添加进一个event handler,该事件参数的类型是ConditionalEventArgs,它包含了一个Result属性,我们可设置它为true或者false,这取决于我们的决定。
但是,对于这些每一个条件的判定,我们也可使用“规则条件”来进行替换。规则条件是对true或者false进行判断的一组规则,例如:购买的物品数目超过了免费送货的界限值。为了清楚地说明这一点,这里有一个使用了规则条件的示例应用程序。
创建一个使用了“规则条件”进行条件判定的新工作流应用程序
1.下载本章源代码,打开RuleQuestioner目录中的解决方案
2.创建一个顺序工作流库项目,步骤可参考第3章“工作流实例”中“向WorkflowHost解决方案中添加一个顺序工作流项目”一节中的步骤。把该工作流库的名称命名为“RuleFlow”。
3.在Visual Studio添加了该RuleFlow项目后,打开工具箱,拖拽一个Code活动到设计器界面上,在它的ExecuteCode属性值中输入AskQuestion。
4.Visual Studio会创建该AskQuestion方法并为你自动切换到代码视图界面下。在该AskQuestion方法中输入下面的代码:
5.找到Workflow1的构造器,在该构造器下面添加这些代码:
private bool _bAnswer = false;
6.添加如下的名称空间:
using System.Windows.Forms;
7.因为MessageBox由System.Windows.Forms支持,当创建一个顺序工作流项目时该程序集不会自动地被Visual Studio所引用,你需要添加该引用。因此在解决方案资源管理器中的RuleFlow项目内的引用树状结点上单击右键,然后从右键快捷菜单中选择“添加引用”,点击.NET选项卡,在列表中找到System.Windows.Forms。选中它然后点击确定。
8.然后切换到工作流视图设计器界面上来。拖拽一个IfElse活动到工作流视图设计器界面上,把它放到你刚刚所放入的Code活动的下面。红色的感叹号标记表明还需要完成额外的工作,在本例中意味着我们还需要添加触发工作流去选择执行左边的路径(“true”)还是右边的路径(“false”)的条件。
9.在工作流视图设计器中,选择左边的ifElseBranchActivity1分支,在Visual Studio的属性面板中将显示该活动的属性。
10.选中Condition属性,点击下拉箭头,这将显示可供使用的条件处理选项的选择列表。选择声明性规则条件选项。
11.通过点击加号(+)展开Condition属性,点击ConditionName属性,这将激活浏览(...)按钮,点击它。
12.这将打开“选择条件”对话框,点击新建按钮。
13.这会打开“规则条件编辑器”对话框。在“条件”域中输入System.DateTime.Now.DayOfWeek == System.DayOfWeek.Tuesday,然后点击确定。
14.注意在“选择条件”对话框的条件列表中就有了一个名称为条件1的条件。
15.在这里,IfElse活动就有了一个条件去进行处理,但是它并没有执行任何代码!因此,拖拽一个Code活动到设计器界面上并把它放进左边的分支中。在它的ExecuteCode属性中输入ShowTuesday。
16.Visual Studio会为你自动切换到代码视图下,在该ShowTuesday事件处理程序中输入下面的代码,然后重新切换到工作流视图设计器界面上来。
"The workflow agrees, it is Tuesday!" :
"Sorry, but today IS Tuesday!";
MessageBox.Show(msg);
17.拖拽第二个Code活动到IfElse活动的右边分支中,在它的ExecuteCode属性中输入ShowNotTuesday。
18.在Visual Studio为你切换到代码视图后,在ShowNotTuesday事件处理程序中输入下面的代码后重新回到工作流视图设计器界面上来:
"The workflow agrees, it is not Tuesday!" :
"Sorry, but today is NOT Tuesday!";
MessageBox.Show(msg);
19.工作流现在就设计完成了,现在从RuleQuestioner应用程序中添加对该工作流的项目级引用。
20.在RuleQuestioner项目中打开Program.cs文件,找到下面的代码:
Console.WriteLine("Waiting for workflow completion.");
21.在上面找到的代码下面添加如下代码,这将创建一个工作流实例。
WorkflowInstance instance =
workflowRuntime.CreateWorkflow(typeof(RuleFlow.Workflow1));
// Start the workflow instance.
instance.Start();
22.编译该解决方案,纠正任何可能出现的编译错误。
23.按下F5(或者Ctrl+F5)执行该应用程序。
假如你仔细看看第13步,你会发现我们添加的规则和用户是否通知了工作流今天是不是星期二完全无关。规则对属于一周的哪一天进行了检查,它也应该能把用户的输入考虑在内。(我也可以向规则中添加this._bAnswer去访问该Boolean值。)
你可能也会对为什么这样做要比使用代码条件好而感到疑惑。其实,并不能说一个要比另一个要好,效果都是一样的。对于基于规则的条件的过程来说,对判定做出改变的东西是保存起来的规则,这可在运行的时候使用不同的规则去进行替换。这是一个很强大的概念。当涉及超过一个以上的规则的时候,它甚至会变得更加强大,这就是使用策略(policy)的情况。但在我们涉及策略之前,我们需要看看前向链接(forward chaining)。
前向链接
假如你曾经观看过轿车的组装过程的话,你绝对会感到惊奇。轿车本身实际上就非常复杂,组装的过程甚至一定会更加复杂。组装过程中隐藏的是一个选项的概念,轿车有一些可选的部件。一些或许有卫星收音机,其它或许要有GPS(全球定位系统)以便驾驶员绝不会迷路。并非组装线上的所有轿车都有每个组装选项。
因此当一辆轿车从线上下来的时侯,组装过程通常会改变。一些组装选项要求在组装过程中很早的时候就布下不同的电气配线、或者更持久的电池、或者不同的引擎部件。
问题是组装过程以每辆车作为基础进行变化。在每个装配站,线上的工人(或者机器人)都会被告知要组装什么部件。告知他们的过程可以容易地设想为一个使用了基于规则的方式的工作流过程。此外,早期作出的判定结果也会影响到后期将怎样去进行判定。有些选项和其它选项不能同时存在,因此在轿车从线上下来时组装过程必须进行改变。
这就是前向链接的本质。规则紧密地链接在一起,就像一个规则的判定结果会影响到接下来的规则会怎样去进行判定。当我们有超过一个以上的规则要去处理时,就像是我们将使用的策略的过程,我们需要去关注规则依赖以及想怎样去处理前向链接。
备注:术语“规则间的依赖关系”真正的意思是两个或更多的规则共享了相同的工作流字段或属性。假如没有规则和其它的规则共享访问相同的工作流字段或属性,则这两个规则之间也就没有依赖关系。假如存在依赖关系,则这个问题将通知规则引擎存在着依赖关系,在有些情况下也有可能要掩盖这些依赖关系的存在。(我们将在这节看到这些内容。)
正如我在本章前面提到过的,规则被聚集到一个规则集(RuleSet)中。在规则集中的规则能被指派优先级,在一个特定的时间点上你能指定它们是否处于激活状态(和enabled属性相似)。当正在处理一个以上的规则时,将以下面的方式来对这些规则进行处理。
1.派生出的处于激活状态的规则列表。
2.所找到的最高优先级的规则(或者规则集)。
3.对规则(或多条规则)进行判定,然后必须执行它的then或者else内的操作步骤。
4.假如规则更新了前面所提到的规则列表中具有更高优先级的规则所使用过的工作流的字段或属性的话,前面的规则会被重新判定并执行它所必须要去执行的步骤。
5.继续进行处理过程,直到根据需要判定完(或者是重新判定)规则集中的所有规则。
通常在三种情况下规则可以是前向链接的:隐式链接(implicit chaining)、特性声明链接(attributed chaining)和显式链接(explicit chaining)。也就是说,规则能被进行链接并且共享依赖,因为工作流运行时能(在某些条件下)弄清是否有这个必要(这是隐式链接),你也可应用基于规则的特性中的某一个标记来声明某个方法(这是特性声明链接),或者使用一个Update语句(这是显式链接)。我们就来对每一个进行简要的看看。
隐式链接
当字段和属性被一条规则进行了更新,而这些字段或属性又显而易见地被其它规则所读出的时候,就会产生隐式链接。例如,考虑这些规则:
And
IF this.Discount > 0 && this.Customer == "Contoso"
THEN this.ShippingCost = 0
第一条规则在订单数量超过500单位时将进行打折。第二条规则陈述了假如公司是Contoso并且也进行了打折,则对运费免费。假如第一条规则起作用的话,第二条规则可能需要再次进行重新判定并执行。
特性声明链接
因为在你的工作流中的方法能对字段和属性进行修改,但是规则引擎可能对此却一无所知,因此WF提供了我在本章前面提到过的基于规则的特性。结合先前的例子,对规则稍微进行改写,特性声明链接可能看起来就像下面这样:
AND
IF this.Discount > 0 && this.Customer == "Contoso"
THEN this.ShippingCost = 0
这里,第一条规则调用了工作流类中的一个方法:SetDiscunt,它对Discount属性进行了更新。但规则引擎并不知道SetDiscount将改变Discount的值,因此当写SetDiscount方法时,你应当使用RuleWrite(或者RuleInvoke)特性:
private void SetDiscount(decimal discountValue)
{
}
RuleWrite特性将通知规则引擎对SetDiscount的调用将导致对Discount属性进行更新。因为这些形成了一个依赖关系,因此假如SetDiscount方法被调用的时候这些规则将会被重新进行判定。
显式链接
最后一种前向链接是显式的,也就是说你的规则中使用了Update语句来告知规则引擎有一个字段或属性的值已经被修改了。Update的作用等同于使用RuleWrite特性。但是正如你所知道的,当调用一个工作流方法的时候,规则引擎并不知道该方法是否对某个规则依赖到的字段或属性进行了更新。在这种情况下,你调用了工作流方法后接着通过使用一个Update语句来告知该规则引擎存在的依赖关系。
这些或许听起来有些古怪,但它还是有使用价值的。假如你写你自己的工作流的话,你应该使用基于规则的特性。但是,当基于工作流的软件变得普遍,人们开始使用第三方的工作流的时候,他们可能会发现基于规则的特性并没有应用到各个工作流方法中去。在这些情况中,他们就应当使用Update语句来保持正确的工作流状态以及规则引擎的同步。基于规则的特性是以声明的方式来指明更新,而Update语句则是在不可避免的时侯使用。当使用了已预编译过的第三方软件的时候,你就需要这种不可避免的方案。
回到先前的例子,假设SetDiscount方法并没有应用RuleWrite特性。则这两条规则就会和下面的这些看起来相像。
IF this.OrderQuantity > 500 THEN this.SetDiscount(0.1)
Update(this.Discount)
And
IF this.Discount > 0 && this.Customer == "Contoso"
THEN this.ShippingCost = 0
有了这些信息,规则引擎就知道了Discount属性已经本更新了,并且也因此将对规则的适用范围进行重新判定。
控制前向链接
你可能会认为一旦你开始了基于规则的工作流的执行后,你就失去了对它的控制并允许规则引擎能进行所有的判定。尽管大多数情况下这正是你想做的,但在处理规则依赖和前向链接上也有一些控制权。
表12-6列出了你所具有的对前向链接进行控制的三种类型。
表12-6 前向链接控制行为
行为 | 功能 |
Full Chaining | 这是默认的,这个行为允许工作流引擎在它认为有必要的时候去对规则进行处理和重新判断。 |
Explicit Chaining | 当应用它时,该控制行为就把前向链接行为的应用范围限定在包含了Update语句的规则上。 |
Sequential | 这实质上是把前向链接关闭。(指明)没有会被判定到的依赖关系,规则依次、按顺序被使用。 |
仅显式更新链接(explicit chaining)行为会使隐式的和特性标记的前向链接无效,并且它应用在使用了显式前向链接,需要由你直接负责去通知规则引擎存在依赖关系的地方。在使用了Update语句的地方,你在规则依赖和重新判定上有总的控制权。在省略了Update语句的地方,规则引擎不会做任何确定是否存在依赖的尝试,因此即使实际上存在依赖关系,规则也将不会被重新进行判定。它的作用是在你的规则中增加了Update语句后,你就在前向链接上掌握了完全的控制权。你可能会这样去做以提高性能(因为规则引擎就不再对所有那些非必要的规则进行重新判定),你也可能必须去这样做以便消除你的规则中的依赖循环。
顺序的链接(sequential chaining)行为实际上是把所有的前向链接关闭。规则从顶到底地在单一的通道上被判定。假如存在依赖关系,这些依赖会被完全忽略。
提示:优先级的正确使用通常也能高效地对前向链接进行控制。高优先级的规则首先执行,因此,高优先级的规则会在低优先级的规则执行以前对低优先级将使用的字段和属性的值进行更新和确定。就像你想起的,在同一个Visual Studio用户界面中要根据你要使用的优先级去创建规则。
控制规则的重新判定
在怎样对规则重新判定上你也有控制权。表12-7列出了这些方法。一个需要牢记的事情是规则的重新判定模式只在个别规则等级上应用。在一个接一个的规则基础上,你能为特定的规则指定重新判定的模式。
表12-7 规则重新判定模式
模式 | 功能 |
Always | 这是默认的。这个模式使工作引擎在必要时对规则进行重新判定。 |
Never | 当应用该模式时,将指明规则只能被判定一次(绝不会被重新判定)。 |
但是,有时你可能不想这样,这时你可选择Never作为你的规则重新判定模式。你为什么会选择这种重新判定模式呢?哦,其中一个例子可能包括以下内容:
这条规则的意思是:“假如手续费低于$5.0并且订单数量超过了500个单位的话,那么就不收取任何的手续费。”但是当满足该规则判定标准并且把手术费设置为0时会发生什么呢?哦,依赖属性Handling被更新了,因此该规则要重新判定!假如你猜到该规则会导致一个无限循环的话,你猜中了。因此,应用一个Never类型的重新判定模式很有意义:手续费用一旦为0,为什么还需再次对规则进行判定呢?尽管在写这个特定的规则时可能使用其它的方式来防止出现无限循环,但问题是在你的工作流创作工具包你有这样一种重新判定模式来作为工具,你又为什么不利用它呢?
使用策略活动
当超过一个以上的规则要被处理的时候,前向链接这种情形就出现了。对于规则条件(Rule Condition)的情形来说,这是不可能发生的情况:因为这种情况下只有一个规则。事实上,它甚至不是一个完整的规则而只是一个布尔表达式。但是,Policy活动改变了所有这些。有了Policy活动,你就有机会把多个复杂的规则组合到一起,并且你可能会看到(某些时候也可能看不到)前向链接的结果。
当你使用Policy活动的时候,规则被聚集进一个集合中,这个集合通过WF的规则集(RuleSet)对象来维护。当你把Policy活动拖拽进你的工作流中后,你需要创建一个规则集对象并插入到你的规则中,在必要时应用前向链接进行控制并使用规则重新判定模式。Visual Studio为帮助创建规则集合提供了一个用户设计界面,就像有一个为添加单一的规则条件(Rule Condition)的用户界面一样。
为了演示Policy活动,让我们重新回味一下我在第4章中“选择一种工作流类型”这一节中所概述的情景。我不会实现前面提到过的所有规则,但我将实现足够多的规则以便对Policy活动的功能进行演示。这些规则集如下所示:
1.当你收到一份订单时,检查你帐目上目前还有的增塑剂的合计数目。假如你认为数目足够的话,就可尝试填写一份完整的订单。否则的话,就准备填写一份部分出货类型的订单。
2.假如你正填的是一份部分出货类型的订单,检查看看订货的公司是接受这种部分出货类型的订单呢还是需要让你先等等,直到你能提供一份完整的订单时为止。
3.假如你正填的是一份完整的订单,就检查在储备罐中增塑剂的实际的量(有些可能已经被蒸发了)。假如具有足够的增塑剂来履行这份完整的订单,就处理这份完整的订单。
4.假如没有足够的增塑剂来履行这份订单,就把它当部分出货的订单类型处理。(看看第二条规则。)
我也知道任何有竞争力的塑胶公司都会知道在储备罐中储存的增塑剂的真实的量,但这仍不失为一个好例子,因为这里实际上包含了许多的条件。假如订单一来并且我们知道我们没有足够的量来满足它,我们就看看我们是否可以提供一份部分出货类型的订单(这些能够根据和客户达成的协议作出选择)。我们也总是可以尝试对我们知道能完全满足的订单进行处理,但当增塑剂的实际的量和我们先前认为的量有所区别的时候会发生什么呢?,这会导致部分出货吗?这种情形我很有兴趣进行演示,因为它显示出了在操作过程中的规则判定处理。
假设我们是塑胶制造商,我们有两个主要的客户:Tailspin Toys和Wingtip Toys。Tailspin Toys已告知我们他们可以接受部分出货,但Wingtip需要订单要完整地发送。我们的工作流将使用一个Policy活动来把这些规则应用到我概述的这些客户、他们的订单以及我们手头的原料数量当中,这可能(或不能)足够完成他们的订单。我们在操作中来看看这个活动。
创建一个使用了Policy活动的新工作流应用程序
1.该PlasticPolicy应用程序再次为你提供了两个版本:完整版本和非完整版本。你需要下载本章源代码,打开PlasticPolicy文件夹中的解决方案。
2.在Visual Studio加载了PlasticPolicy解决方案后,新创建一个顺序工作流库的项目,该工作流库的名称命名为PlasticFlow。
3.在Visual Studio添加了该PlasticFlow项目后,Visual Studio会打开Workflow1工作流以便在工作流视图设计器中进行编辑。打开工具箱,拖拽一个Policy活动到设计器界面上。
4.在你真正创建规则以便把它们和你刚才插入进你的工作流中的Policy活动相配合前,你需要添加一些初始化代码和方法。以代码视图的方式打开Workflow1.cs文件,在该类的构造器前添加这些代码:
private decimal _plasticizer = 14592.7m;
private decimal _plasticizerActual = 12879.2m;
private decimal _plasticizerRatio = 27.4m; // plasticizer for one item
private Dictionary<string, Shipping> _shipping = null;
// Results storage
private bool _shipPartial = false;
private Int32 _shipQty = 0;
// Order amount
private Int32 _orderQty = 0;
public Int32 OrderQuantity
{
get { return _orderQty; }
set
{
// Can't be less than zero
if (value < 0) _orderQty = 0;
else _orderQty = value;
}
}
// Customer
private string _customer = String.Empty;
public string Customer
{
get { return _customer; }
set { _customer = value; }
}
5.添加下面的名称空间:
using System.Collections.Generic;
6.再次找到Workflow1的构造器,在该构造器内调用初始化组件(InistializeComponent)的方法下添加下面这些代码:
this._shipping = new Dictionary<string, Shipping>();
this._shipping.Add("Tailspin", Shipping.Partial);
this._shipping.Add("Tailspin Toys", Shipping.Partial);
this._shipping.Add("Wingtip", Shipping.Hold);
this._shipping.Add("Wingtip Toys", Shipping.Hold);
7.在构造器下添加下面这些方法:
{
// Check to see that we have enough plasticizer
return _plasticizer - (OrderQuantity * _plasticizerRatio) > 0.0m;
}
private bool CheckActualPlasticizer()
{
// Check to see that we have enough plasticizer
return _plasticizerActual - (OrderQuantity * _plasticizerRatio) > 0.0m;
}
[RuleWrite("_shipQty")]
private void ProcessFullOrder()
{
// Set shipping quantity equal to the ordered quantity
_shipQty = OrderQuantity;
}
[RuleWrite("_shipQty")]
private void ProcessPartialOrder()
{
// We can ship only as much as we can make
_shipQty = (Int32)Math.Floor(_plasticizerActual / _plasticizerRatio);
}
8.为了使你能在规则处理时看到输出结果,你需要激活属性面板。在属性面板中,点击事件工具条按钮,然后在Completed事件中输入ProcessingComplete。这会在你的工作流代码中为WorkflowComplete事件添加一个对应的event handler并为你切换到Workflow1类的代码编辑界面下。
9.定位到Visual Studio刚刚添加的ProcessingComplete方法,插入下面这些代码:
OrderQuantity == _shipQty ? "can" : "cannot");
Console.WriteLine("Order will be {0}", OrderQuantity == _shipQty ?
"processed and shipped" : _shipPartial ?
"partially shipped" : "held");
10.现在切换回工作流视图设计器来,我们将添加一些规则。首先,选中policyActivity1,以便在属性面板中激活它。点击RuleSetReference属性,这将激活浏览(...)按钮。
11.点击该浏览按钮,这将打开“选择规则集”对话框。一旦“选择规则集”对话框打开后,点击新建按钮。
12.点击新建按钮将打开“规则集编辑器”对话框,然后点击添加规则。
13.你将添加三条规则中的第一条。你添加的每个规则都由三个部分组成:条件、Then操作和Else操作(最后一个是可选的)。在条件部分中输入this.CheckPlasticizer()。(注意它调用的是一个方法,因此括号是必须的。)在Then操作部分,输入this.ProcessFullOrder()。最后在Else操作部分中输入this.ProcessPartialOrder()。
14.再次点击添加规则,把第二条规则添加到规则集中。在本条规则的条件部分中输入this.CheckActualPlasticizer()。在Then操作部分输入this.ProcessFullOrder()。在Else操作部分,输入this.ProcessPartialOrder()。
15.再次点击添加规则以便插入第三条规则。在第三条规则的条件部分,输入this._shipping[this._customer] == PlasticFlow.Workflow1.Shipping.Hold && this._shipQty != this.OrderQuantity。在Then操作部分,输入this._shipPartial = False。在Else操作部分输入this._shipPartial = True。
16.点击确定关闭“规则集编辑器”对话框。注意现在在规则列表中多了一个名称为规则集1的规则。再点击确定关闭“选择规则集”对话框。
17.你的工作流现在就完成了,尽管它看起来有些古怪,因为整个工作流中就只有单一的一个活动。其实,你已经通过你提供的规则来通知你的工作流要做些什么。还有就是需要在PlasticPolicy应用程序中添加对该工作流的项目级引用。
18.在PlasticPolicy项目中打开Program.cs文件,找到Main方法。在Main方法的左大括号下面添加下面这些代码:
string company = String.Empty;
Int32 quantity = -1;
try
{
// Try to parse the command line args
GetArgs(ref company, ref quantity, args);
}
catch
{
// Just exit
return;
}
19.然后在Main方法中找到下面这行代码:
Console.writeLine("Waiting for workflow completion.");
20.在上面的代码下添加如下这些代码:
Dictionary<string, object> parms = new Dictionary<string, object>();
parms.Add("Customer", company);
parms.Add("OrderQuantity", quantity);
// Create the workflow instance.
WorkflowInstance instance =
workflowRuntime.CreateWorkflow(typeof(PlasticFlow.Workflow1), parms);
// Start the workflow instance.
instance.Start();
21.在第18步,你添加的代码调用了一个对命令行参数进行处理的方法。现在你需要添加该方法。在Program.cs源文件的底部添加这个方法:
22.编译该解决方案,修正任何出现的编译错误。
我们现在将使用这个示例应用程序来执行四个场景。第一个场景是规则引擎要处理的最棘手的场景之一:Tailspin Toys预订的量是500单位。这是一个很有用意的数字,因为这样要消耗的增塑剂总计为14,592.7(这是一个完全编造的数字),但是在储备罐中的增塑剂的真实的量总共是12879.2(我总共能凑足的数字!)。因为每生产一件成品需要消耗27.4个单位的增塑剂(这是我编造的另一个值,它通过Workfow1中的_plasticizerRatio来表示),该订单正好处于这样一个范围:表面上该订单能全部满足,但实际上没有足够的增塑剂。也就是说,我们认为我们的全部增塑剂能加工出532个成品(14,592.7除以27.4),但是看到储备罐中真实的量后,我们只能加工出470个成品(12,879.2除以27.4)。最后,我们只能进行部分出货。
甚至,假如你运行该应用程序时,提供的公司名称为“Tailspin Toys”,提供的数量为“500”(对应的命令行内容为:PlasticPolicy.exe /c:"Tailspin Toys" /q:500),你就可看到如图12-2所示的输出结果。此外,Tailspin众所周知可接受部分出货,工作流也指明了这一点。
备注:因为PlasticPolicy应用程序接受命令行参数,在调试模式下你需要使用Visual Studio项目设置来提供这些参数然后运行该应用程序,或者打开一个命令提示符窗口,浏览并定位到包含PlasticPolicy.exe的目录下,然后在命令提示符下执行该应用程序。
图12-2 Tailspin Toys的部分出货订单
但是假如Tailspin预订的数量是200个的话,该工作流还会正确地执行吗?我们就来看看。再次在命令提示符下运行该程序:PlasticPolicy.exe /c:"Tailspin Toys" /q:200。运行结果如下图12-2所示:
图12-3 Tailspin的完整出货订单
Tailspin注册为能接受部分出货的类型。但是Wingtip Toys则希望订单继续有效,直到整个订单都能满足时为止。该工作流也处理Wingtip吗?而且,假如Wingtip的订单处在这样一个范围:我们认为我们有足够的增塑剂但实际上没有呢?为找出答案,试试这个命令:PlasticPolicy.exe /c:"Wingtip Toys" /q:500。就像如图12-4中显示的,我们发现我们只能部分完成Wingtip的订单。最重要的是,当我们访问需要优先进行处理的顾客的记录时,选出被压下的Wingtip的订单是当务之急。
图12-4 Wingtip Toys的部分出货订单
为对最后一个场景进行测试,我们要能满足Wingtip的需求,而不用考虑增塑剂的真实的量,因此在命令提示符下输入下面的命令:PlasticPolicy.exe /c:"Wingtip Toys" /q:200。Wingtip Toys现在已经定了200个成品,事实上,图12-5也指明了我们能完整地履行Wingtip的订单。
图12-5 Wingtip Toys的完整出货订单
基于规则方式的强大在于处理的处理方式。想像这个塑胶策略的例子,假设它被创建成使用了几个嵌套的IfElse活动组成,或许还要一个ConditionedActivityGroup活动,它们通过使用工作流视图设计器以面向过程的方式创建。(当我们对储备罐中的增塑剂进行检查时,ConditionedActivityGroup活动也要对规则重新判定进行说明)。在这种情况下,面向过程的模式不是一种好的工作方式,尤其对于要考虑使用许多嵌套的IfElse活动和优先级来说更是如此。
但是,以规则为基础的方式使处理模式简化。许多嵌套的活动合而为一。而且,因为规则是一种资源,你能很方便地把它们抽取出来,然后用不同的规则对它们进行替换,这比你通常地去部署一个新的程序集来说要更加容易。你可能会发现真实世界的工作流由过程化的方式和基于规则的方式二者合并组成。正确的做法是保证你的工作流一定能工作的前提下,根据实际情况的限制条件选择最合适的方式。
本章源代码:里面包含本章的练习项目和完整代码