(翻译)《WF编程系列之48》 第九章 规则和条件

    软件将知识(knowledge)应用于数据。对于所有的软件——从企业级应用程序到视频游戏。软件中的知识通常是代码型知识和声明性知识的联合。代码型知识是关于如何执行任务的信息,就像如何使用网上旅行社来预订宾馆和租车那样。代码型知识是很容易用一门通用的编程语言来解释的,如C#、VB.NET或任何它们的“祖先”。

    另一方面,声明性知识,是关于数据间的关系的。我们经常把声明性关系称为企业级规则。例如,企业级规则可能会将预订宾馆指定为至少14天并才能预先获取10%的折扣,除非房间的费用不到100$。日期和价格是有关系的并相互影响。用一门通用的编程语言来表示这种类型的知识,在小范围内并不是很难,但是随着知识数量的增长会崩溃。我们必须使用if-then-else语句将其转换为代码性知识。很多软件应用程序需要大量的企业级规则:税收筹划系统,抵押银行软件,以及宾馆预订系统,仅举几例。

    代码型知识中的企业级编码规则,使得规则难于查找、读取和修改。多年之后,软件商为使用企业级规则的工作发明了一些工具。我们按照规则引擎把这些工具分类。规则引擎专注于使声明性知识更容易实现、处理、隔离和修改。

    WF提供了一个规则引擎,并为两种知识都提供了最佳方案。我们可以使用Sequence活动来实现代码性知识,而Policy活动是用来执行声明性知识的。

    在本章中,我们将关注于为声明性知识使用了规则和条件的活动。我们将开始于介绍一个使用了条件的活动,如While活动。我们还将讨论Policy活动,这是一个规则引擎;还会讨论ConditionedActivityGroup,它会基于When和Until条件,有条件地执行活动。

9.1 什么是规则和条件?

    在本章中我们将使用三个重要的概念:条件、规则,以及规则集。在WF中,条件是一段返回true或false的逻辑。大量的WF活动使用条件来管理它们的行为。这些活动包括While活动、IfElseBranch活动和ConditionedActivityGroup活动。例如,While活动,直到它的Condition属性返回false才停止循环。我们可以在代码中或XML中实现条件。

    规则,是带有一组要执行的操作的条件。规则使用了一种声明性的if-then-else风格,这里if是一个判定条件。如果条件判定为true,运行时就会执行then操作,否则就是else操作。尽管这听起来像是代码型知识,这里还是有本在的不同。在大多数语言中,if-then-else结构有效地改变了在应用程序中的控制流程。另一方面,规则被动地等待执行引擎来判定它们的逻辑。

    规则集是由一个或多个规则组成的集合。作为来自宾馆企业的另一个例子。我们可能需要3个规则来计算房间价格的折扣(这里显示的是伪代码)。

if 人的年龄 大于 55

   then 折扣 = 折扣 + 10%

if 停留时间 大于 5天

   then 折扣 = 折扣 + 10%

if 折扣 大于 12%

   then 折扣 = 12%

    在我们能够判定这些规则之前,我们需要对它们在规则集中进行分组。规则集允许我们为每个规则分配优先级,并控制它们的判定顺序。如果后面的规则改变了在前面规则中使用的数据,WF可以重新访问这些规则。我们可以把规则存储在外部的XML文件中,并在创建一个新的工作流时,把规则传递到工作流运行时。WF为我们提供了API——对规则集和规则的更新、创建和修改进行编程。和编译过的代码相比,上面描述的特性和执行语法给了我们更多的弹性和控制。例如,这些API允许我们动态定义规则来满足一个特定的客户或企业场景的需要。

    在本章后面,我们会返回到规则和规则集。目前,我们将深入到WF中的条件中。

9.2 使用条件编程

    While活动是一个使用了条件的工作。While活动将重复执行它的子活动,直到它的Condition属性返回false。While活动的Properties窗体允许我们将这个Condition属性设置为代码条件(Code Condition)或声明性条件(Declarative Condition)。在下面的截图中,我们告诉While活动使用代码条件,而这个代码条件是在一个名为CheckBugIndex的方法中实现的。

image

9.2.1 代码条件

    代码条件在我们的后台代码文件中是一个事件处理程序。代码条件通过ConditionalEventArgs参数返回一个布尔值。因为代码条件在我们的工作流类中只是另一个方法,所以条件的逻辑被编译到相同的程序集中,其中寄宿了我们的工作流定义。

    CheckBugIndex的实现如下所示。我们具有一个由bug对象组成的数组,用于工作流的处理。这个数组可能作为一个参数到达工作流,或者通过其它一些诸如HandleExternalEvent活动的通信机制。工作流使用了bugIndex字段,通过数组来跟踪它的进程。在某些地方,随着工作流处理完每个bug,另一个活动将会递增bugIndex。如果bug数组没有初始化,或者如果bugIndex并没有指向该数组中的一个有效项,我们想要通过让代码条件返回false值来终止While循环。

private Bug[] bugs;
private int bugIndex = 0;

protected void CheckBugIndex(object sender, ConditionalEventArgs e)
{
    if (bugs == null || bugIndex >= bugs.Length)
    {
        e.Result = false;
    }
    else
    {
        e.Result = true;
    }
}

    代码条件,就像我们的上面的方法那样,在运行期间被表示为CodeCondition对象。CodeCondition类派生自一个抽象的ActivityCondition类(参见下面的截图)。

image

    因为While活动的Condition属性接受一个ActivityCondition对象,所以我们可以选择指派CodeCodition或RuleConditionReference。与我们选择哪种类型无关,运行时需要做的全部是,调用Evaluate方法来取出一个布尔型结果。CodeCondition最终将触发它的Condition事件来检索这个布尔值。我们把这个Condition事件和后台代码文件中的方法连接在一起。我们可以通过查看由设计器生成的XAML标记,从而看得更清楚一些。

image

    在下一节中,我们将看到RuleConditionReference是如何工作的。

9.2.2 规则条件

    声明性规则条件的工作方式不同于代码条件。如果我们将CheckBugIndex条件表示为一个声明性规则,我们只需要输入下面的字符串到规则条件设计器中:

bugs == null || bugIndex >= bugs.Length

    WF将在运行期间解析并判定这个规则。我们不需要在工作流类中创建一个新的方法。用于这个表达式的定义最终作为工作流地一部分存在于.rules文件中。RuleConditionReference对象将通过名称来引用这个表达式,WF中的每个规则都有一个名称。

    作为一个例子,假设我们正在创建一个带有While活动的新工作流,我们想要这个活动循环,直到_retryCount字段超过某个值。在我们把While活动拖放到设计器后,我们可以俄打开Properties窗口并点击Codition属性的下拉列表。这一次,我们将选择条件规则条件。设计器将使得ConditionName和Expression这两个项是可用的。点击ConditionName旁边的文本框,将显示一个省略号,如下图所示:

image

    点击这个省略号按钮,这将会加载Select Condition的对话框,如下面的截图所示。这个对话框将在我们的工作流中列举出所有声明性规则条件,并初始化为空。在对话框的上面一排是创建、编辑、重命名和删除规则的按钮。右手边的Valid列可以让我们了解在我们的工作流中关于语法错误和其它有效性问题。当我们选择一个条件时,Condition Preview区域将为我们显示条件代码。

image

    到目前为止,我们想创建一个新的规则。点击New…按钮将加载Rule Condition Editor对话框,如下所示:

image

    在这个编辑器中我们可以输入表达式。当_retryCount字段小于4时,我们输入的表达式将返回true。如果我们输入C#中的this关键字(或VB.NET中的Me关键字),那么智能感应窗体将出现,并在我们的工作流中显示一组字段、属性和方法。

    在编辑器中点击OK按钮将返回Select Condition对话框,这里我们可以点击Rename按钮,从而给我们的条件一个友好的名称(默认名称将会是Condition1,它不是描述性的)。我们将规则的名称设为RetryCountCondition。

9.2.2.1 rules文件

    在点击所有的按钮之后,在解决方案窗体中,一个新的文件就会出现在我们的工作流之下。文件将具有和我们的工作流类相同的名称,但却带有.rules的扩展名。在文件内,是我们所编写条件的冗长XML表示。

image

    如果你还记得在第2章中我们讨论的XAML,你将认识到这是来自System.CodeDom命名空间的对象的XAML表示。CodeDom(Code Document Object Model)命名空间包括了在语言无关的方式中创建源代码的类。例如,CodeBinaryOperatorExpression类表示了两个表达式之间的一个二进制操作符。在我们的XAML中的实例是一个<=操作符,但可以是一个+、-、>=或位运算。

    WF使用System.CodeDom.Compiler命名空间中的类,来动态生成并编译来自创建于XAML的CodeDom对象图表的源代码。一旦运行时编译这个表达式,WF就可以判定规则并截取到它的结果。

9.2.2.2 有效的表达式

    我们为代码所写的小段代码是有效的C#或VB.NET表达式。表达式必须判定为一个有效的布尔值。例如,下面所有的表达式都是有效的。我们可以调用方法,检索属性,索引数组,甚至使用来自基类库的其他类,就像用于正则表达式的Regex类。

image

    表达式必须判定为true或false。下面的例子是无效的:

image

9.2.2.3 规则和激活

    在第2章,我们讨论了工作流激活。激活允许我们传递工作流的XAML定义到工作流运行时,而不是使用一个编译过的工作流定义。例如,让我们假设在命名为Activation.xoml的文件中具有下面的工作流定义。

image

    让我们假设我们的条件(Conditional)位于命名为Activation.rules的文件中。我们可以加载工作流和带有下面代码的规则文件:

XmlReader definitionReader;
definitionReader = XmlReader.Create(@"..\..\conditions\Activation.xoml");

XmlReader rulesReader;
rulesReader = XmlReader.Create(@"..\..\conditions\Activation..rules");

Dictionary<string, object> parameters = null;
WorkflowInstance instance;
instance = workflowRuntime.CreateWorkflow(definitionReader, rulesReader, parameters);

    激活带给我们大量的弹性。例如,我们可以在数据库记录中存储工作流和规则定义,并更新规则,而不用重新编译或重新部署一个应用程序。

9.2.3 有条件的活动组

    在我们完成讨论条件之前,我们需要仔细看一个以条件为中心的活动,它是有弹性的并且强大的。ConditionActivityGroup(CAG)执行一个由子活动组成的集合,它们基于附属于每一个子孙的WhenCondition。进一步讲,CAG会继续执行,直到CAG上的UntilCondition返回true。这个行为使得CAG多少属于While活动和Parallel活动的交叉点。

    当我们拖动CAG到工作流设计器中,它将出现在下面的截图中。在活动图形的上部是活动的故事板,我们可以把活动拖动到上面。在故事板每一边上的箭头允许我们在故事板中的子活动间翻阅。当我们在故事板中选择一个活动,被选择的活动将出现在预览盒中的活动图形底部。我们可以使用CAG图形的中间位置的按钮,在预览和编辑模式之间转换。

image

    在下面的截图中,我们在CAG的故事板中排列了一些活动。第一个活动是一个Sequence活动,并且我们已经选择了用于编辑的活动。CAG的图形底部详细显示了Sequence活动。在Sequence活动中,我们放置了两个Code活动。

image

    既然Sequence活动是CAG的直接子孙,我们可以为Sequence活动指派一个WhenCondition(参见下面的截图)。就像所有的条件那样,WhenCondition可以是一个代码条件,或者一个声明性规则。

image

    如果WhenCondition返回true,那么CAG就只执行一个子活动;然而,WhenCondition是可选的。如果我们没有指定WhenCondition,子活动就只会执行一次。CAG继续循环多少次是无关紧要的,一个没有WhenCondition的活动将只会在第一次迭代期间执行

    CAG重复执行子活动,直到两件事情中的一件发生。第一,CAG本身有一个UntilCondition(参见下面的截图)。当UntilCondition返回true时,CAG会立即停止处理并取消任何当前的执行子活动。第二,如果没有子活动在执行,那么CAG将停止处理。当WhenCondition的所有子活动都返回false时,这种情况就会发生。

image

    注意到,很重要的一点是,当CAG第一次开始执行时,它会判定UntilCondition。此刻,如果该条件返回true,那么将不会有任何子活动执行。同时,每当子活动完成执行时,CAG都会判定UntilCondition。这意味着只有子活动的一个子集会执行。最后,CAG不会保证子活动的执行顺序,这就是为什么CAG类似于Parallel的原因。例如,拖动一个Delay活动到CAG中,这并不会阻塞来自于执行它的其它子活动的CAG。

9.2.3.1 什么时候使用CAG

    CAG在“寻的”场景中很有用。这么说吧,我们正在创建一个工作流,用来预定航班并租车用于旅行。在工作流中,我们可能使用Web Service从第三方系统中请求价格信息。我们可以在CAG中安排Web Service调用,从而重复请求价格,直到我们满足目标。我们的目的可能是为了这次旅行的全部价格满足于最小的花费,或者我们可能是为了使用更高级的目标,包括花费、全部旅行时间,以及宾馆的类。

9.3 使用规则编程

    我们在前面章节中看到的声明性规则条件,只返回一个true或false值。条件没有修改工作流。另一方面,规则级是一个条件,也是一组if-then-else形式的操作。WF中的Rule类表示这个if-then-else概念。下面的类图显示了与Rule类有重要关系的一些类。

image

    第一个需要注意的概念是,RuleSet类管理着一个规则的集合。Policy活动将使用RuleSet的Execute方法来处理这个规则集合。稍后我们将详细讨论Policy活动。

    RuleSet中的每个Rule都具有一个Condition属性,它引用了一个单独的RuleCondition对象。RuleSet逻辑将使用RuleCondition的Evaluate方法来检索一个true或false值。

    每个Rule维护着两个由RuleAction对象组成的集合——ElseActions和ThenAction。当规则的条件判定为true时,运行时会在ThenActions集合中调用每个操作的Execute方法;否则,运行时会在ElseActions集合中调用操作的Execute方法。

    既然我们对规则在内部如何工作有了基本的理解,接下来让我们看一下Policy活动。

9.3.1 Policy活动

    在微软的百科辞典中,将“策略(policy)”描述为——由个人、组织或政府采纳的行动纲领。策略在现实生活中无处不在。学校为学生管理定义了策略,银行为贷款定义了策略。美国银行经常把它们的贷款策略基于信誉积分上,而信誉积分会把很多因素考虑在内,如个人的年龄、以往的支付记录、收入以及未偿债务。商业策略可能更加复杂——充满了声明性知识。正如我们在本章前面所讨论的那样,声明性是关于数据间的关系。例如,一个银行的策略可能认为——如果我们的信誉积分少于500点,这将要我们额外付出一个百分点的钞票。

    我们在本章的开始部分讨论过,声明性知识是如何不能很好地适用于诸如C#、VB.NET这样普通的编程语言。取代地,我们调用规则引擎的特定工具,可以极佳地管理和执行声明性知识。WF中的Policy活动就是这样一种规则引擎。

9.3.1.1 创建一个Policy工作流

    虽然我们几乎可以在一个大型工作流中的任何地方使用Policy活动,但是我们还是会使用一个内部只带有单独一个Policy活动的简单的工作流。我们所要做的全部是创建一个新的顺序工作流,并拖动一个Policy图形到设计器中(参见下面的截图)。

image

    在上面的截图中,我们可以看到Properties窗口。RuleSetReference属性是Policy活动的主要属性。我们可以在Policy活动的Properties窗口中点击带有省略号的按钮,来加载Select Rule Set对话框,如下所示:

image

    当我们第一次创建工作流时,我们不会有任何已定义的规则集。工作流可以包括多个规则集,而每一个规则集包括一个或多个规则。虽然Policy活动可以引用一个单独的规则集,但我们可能会设计一个其中带有多个Policy活动的工作流,并要每个Policy活动引用一个不同的规则集。

    点击对话框中的New按钮,这会加载Rule Set Editor对话框,如下所示:

image

    Rule Set Editor对话框为规则集暴露出很多选项。目前,我们重点关注条件和操作。假设我们为软件的bug打分定义了一个策略。我们计算的分数将决定是否需要发送通知到小组成员以立即采取行动。这个bug将成为我们的工作流类中的一个成员字段,并暴露各种属性(Priority、IsOpenedByClient),我们将检查这些属性来计算分数。下面列出的BugDetails类定义了这个bug:

public enum BugPriority
{ 
    Low,
    Medium,
    High
}

public class BugDetails
{
    private string _title;

    public string Title
    {
        get { return _title; }
        set { _title = value; }
    }

    private bool _openedByClient;

    public bool OpenedByClient
    {
        get { return _openedByClient; }
        set { _openedByClient = value; }
    }

    private bool _isSecurityRelated;

    public bool IsSecurityRelated
    {
        get { return _isSecurityRelated; }
        set { _isSecurityRelated = value; }
    }

    private bool _isVerified;

    public bool IsVerified
    {
        get { return _isVerified; }
        set { _isVerified = value; }
    }

    private BugPriority _priority;

    public BugPriority Priority
    {
        get { return _priority; }
        set { _priority = value; }
    }

    private int _score;

    public int Score
    {
        get { return _score; }
        set { _score = value; }
    }
}

    我们的第一组规则通过查看bug的优先级设置来决定一个bug的基本分数。首先,我们在对话框中点击Add Rule按钮。第一个规则具有条件

this.Bug.Priority == BugPriority.Low,以及操作

Then this.Score = 0。在对话框中,我们可以给这个规则起一个有意义的名称:SetScore_LowPriority。

    在我们的规则中的条件,和在本章前面介绍的条件是相同的。我们可以测试=、!=、>=或<=。我们可以调用方法并索引数组。只要条件的表达式返回true或false,并且可以表示为System.CodeDom命名空间中的类型,我们就得到了一个有效的表达式。

    在我们的规则中的操作,是具有极大的弹性的。操作可以调用方法,并可以和字段和属性交互,但没有被约束为只能返回一个布尔值。实际上,大多数操作将执行工作流中的任务并操作器中的字段和属性。在SetScore_LowPriority规则中,我们使用规则操作来分配初始分数0。还要记住,在规则上操作的属性和集合,意味着我们可以为then和else指定多个操作。在操作的文本框中,我们需要把每个操作放在单独的一行上。

    我们的bug可以获取三个可能的优先级中的一个值(Low、Medium或High),因此我们需要一个规则来为每个可能的优先级设置bug的分数。一旦我们输入了这三个规则,Rule Set Editor看上去就应该和下面的截图相同。注意到我们将每个规则的Priority和Reevaluation属性保留为它们的默认值。稍后我们再介绍这些属性。

image

9.3.1.2 判定

    这些规则,如我们前面使用过的声明性条件,将存放在一个.rules文件中。当一个规则集执行时,它将通过判定规则的条件并执行then或else操作,来处理每一条规则。规则集会以这个风格继续处理,直到处理完规则集中的每个规则,或直到它遇到了一条Halt指令。Halt是一个关键字,我们可以把它放在规则的操作列表中,这会使规则集停止处理额外的规则。

    根据这些规则,我们可以执行我们的工作流,并观察Policy活动通过执行事先定义的规则集来为我们的bug计算分数。然而,我们仍然想要添加额外的规则到我们的规则集中。这条新规则是这样的:“如果bug的分数大于75,那么就发送一封邮件到开发小组”。然而,这条规则代表了一个潜在的问题——如果这个规则集在判定指派分数的规则之前判定了这个规则,那么它将不能工作。我们需要保证首先为bug设置分数,然后使用规则优先级来达到这一目的。

9.3.1.3 优先级

    每个规则都有一个Int32类型的Priority属性。我们可以在下图的Rule Set Editor中看到这个属性。在执行规则之前,规则集将按照优先级将它的规则排序到一个列表中。在规则集中,具有最高优先级的规则将首先执行。具有相同优先级的规则将按照它们的Name属性的字母顺序来执行。

    为了确保我们的通知规则被最后判定,我们需要将该规则的优先级指定为0,并保证其它所有具有较高的优先级。在下面的截图中,我们将开始的三个规则设置为100。我们为优先级使用的数字是随意的,因为它只控制相对的执行顺序。在优先级的数值间留有间隔是一个好办法,从而我们以后可以在其中放入更多的规则。

image

9.3.1.4 规则依赖性

    到目前为止我们编写的所有规则都是非依赖的。我们的规则不修改工作流中其它规则所依赖的的任何字段。然而,设想一下,我们有一个规则是这样的:

    “如果bug的IsSecurityRelated属性为true,那么就将bug的Priority设置为High。”

    显然,这个规则会影响SetScore_HighPriority规则——当bug的优先级被设置为High时,将我们的bug分数设置为100。

    解决这个问题的一个方案是——设置规则的相关属性来保证Set Score规则总是在任何可能设置Priority字段的规则之后执行。然而,随着规则集的增长以及规则间的依赖性变得更加杂乱,这个解决方案的类型并不总是可行的。

    幸运的是,WF简化了这一场景。如果你看一下前面截图中的类图(在“使用规则编程”一节中),你将发现RuleCondition类携带了一个GetDependencies方法,而RuleAction类则携带了一个GetSideEffects方法。这两个方法允许规则引擎将规则的依赖性和其它规则的“副作用(side effect)”进行匹配(规则的条件会检查规则的字段和属性,从而计算出一个值)。当一个操作产生了副作用——匹配来自前面的执行规则的依赖,规则引擎就可以向后回溯并重新判定前面的规则。在规则引擎的术语中,我们将这个特性称为前向链接(forward chaining)。WF中的链接特性可以显式或隐式地工作。

隐式链接

    WF中的前向链接默认是隐式的。就是说,WF小心地管理着副作用、依赖性、规则的重新计算,并且我们不需要采用额外的步骤。规则引擎在每个规则条件中检查表达式,并检查每个规则操作,以生成依赖性和副作用的列表。我们继续前进,并编写我们的规则AdjustBugPriorityForSecurity,如下所示:

image

    现在,如果工作流找到一个IsSecurityRelated属性设置为trye的bug,这个新规则的操作将修改bug的Priority为High。完整的规则看上去是下面这样的:

IF this.Bug.IsSecurityRelated

THEN this.Bug.Priority = BugPriority.High

    规则引擎将知道前面三个规则在Priority属性上具有依赖,并重新判定所有的三个规则。所有这些都在NotificationRule运行之前发生,因此设置了IsSecurityRelated的bug将创建一个分数100,而NotificationRule将调用SendNotification方法。

    隐式链接是一个非常好的特性,因为我们不需要手动计算依赖。然而,为了让隐式链接工作,规则引擎必须能够通过规则表达式推迟依赖。如果我们有这样一个规则,它调用了编译过的代码或第三方的代码,规则引擎就不再处理依赖。在这些场景中,我们可以利用使用了元数据特性或显式操作的链接。

带有特性的链接

    假设我们需要为规则执行的逻辑是复杂的——复杂到我们在声明式编写所有的逻辑时会感觉很不舒服。我们所能做的是,把逻辑放在后台文件的方法中,并从我们的规则中调用方法。举一个例子,让我们编写最后一个规则,如下所示:

IF This.Bug.IsSecurityRelated

THEN this.AdjustBugForSecurity()

    正如我们提到的那样,如果我们需要前向链接的话,这种方法调用提出了一个问题。规则引擎不知道AdjustBugForSecurity方法将调用哪个字段和属性。好消息是,WF提供了我们可以使用的特性,来声明方法的依赖和副作用。

特性

描述

RuleWrite

声明方法将要修改的一个字段或属性(方法的副作用)

RuleRead

声明方法将要读取的一个字段或属性(方法的依赖)

RuleInvoke

声明当前方法将要调用的一个方法。引擎将为额外的特性检查第二个方法。

    如果方法不带有上面的特性,那么规则引擎将假设方法不会读取或写入的任何字段或属性。如果我们想要向前链接和我们的方法一起工作,我们需要将它定义如下:

[RuleWrite("Bug/Priority")]
public void AdjustBugForSecurity()
{
    // ... other work
    Bug.Priority = BugPriority.High;
    // ... other work
}

    RuleWrite特性使用了类似于WF中属性绑定的语法。这个特别的RuleWrite特性声明了该方法将写入Bug类的Priority属性。规则引擎还可以解析通配符语法,从而[RuleWrite(“Bug/*”)]将告诉引擎——该方法将写入到bug对象的所有字段和属性。除非我们要在方法上使用这个特性。来告诉引擎关于方法的依赖性——该方法调用自我们规则的带有条件的部分,否则RuleRead特性会使用相同的语法。

    在我们的方法调用其他方法时,我们可以使用RuleInvoke特性,如下所示:

[RuleInvoke("SetBugPriorityHigh")]
public void AdjustBugForSecurity()
{
    // ... other work
    SetBugPriorityHigh();
    // ... other work
}

[RuleWrite("Bug/Priority")]
void SetBugPriorityHigh()
{
    // ... other work
    Bug.Priority = BugPriority.High;
    // ... other work
}

    在这个代码示例中,我们告诉了规则引擎——调用来自我们规则的方法将轮流调用SetBugPriority方法。规则引擎将检查SetBugPriorityHigh方法,并发现RuleWrite特性将会保存前向链接。

显式链接

    在一些场景中,我们可能需要从我们的规则中调用第三方代码。这个第三方代码可能具有副作用,但是这些代码不是我们的,所以我们不能添加RuleWrite特性。在这种场景中,我们可能在我们的规则操作中使用一个显式的Update语句。例如,如果我们为AdjustBugForSecurity方法使用了一个显式的Update语句而不是ReadWrite特性,我们编写声明性规则条件,如下所示:

this.AdjustBugForSecurity();
Update("this/Bug/Priority");

    注意到,更新语句语法和我们的RuleWrite语法很类似,并且不存在相应的Read语句可以使用。如果有可能,最好使用基于特性的方法。当我们不能添加方法特性时,或者当我们需要精确控制链接行为时,就需要为该场景设计这种显式的方法,下面的章节会讨论链接行为。

9.3.1.5 控制链接

    规则集的前向链接行为是强大的。我们可以执行规则并让它们重新判定——即使我们不知道他们的相互依赖。然而,有时链接常会生成讨厌的结果。例如,有可能把规则引擎放入无限循环中。或者写一个规则,而我们并不想重新判定这个规则。幸运的是,这里有一些有用的选项,用来把规则处理联系在一起。

链接行为

    第一个选项是RuleSet类上的ChainingBehavior属性。Rule Set Editor在标记为Chaining的下拉列表中暴露了这个属性。可以使用的选项是Sequential、Explicit Update Only和Full Chaining。Full Chaining是默认的规则集行为,为我们提供了前向链接规则判定——之前我们介绍过这个概念。

    Explicit Update Only选项告诉规则引擎不要使用隐式链接。此外,规则引擎将忽略RuleWrite和RuleRead特性。选中Explicit Update Only,这个触发规则重新判定的唯一机制就是我们在上一节中描述的Update语句了。显式更新让我们可以对规则进行精确地控制——引起前面规则的重新判定。

    然而Sequential选项会禁用链接。操作顺序式行为的规则集将只会执行它所有的规则一次,并按照由它们的相应Priority属性所指定的顺序(当然,Halt语句仍然可以在所有的规则完成执行之前终止规则处理)。

重新判定行为

    控制链接的另一个选项是使用规则的ReevaluationBehavior属性。这个属性是在Rule Set编辑器中由标记为Reevaluation的下拉列表所暴露的。可以使用的选项是Always和Never。

    Always是规则的默认行为。如果满足适当的标准,那么带有这个设置的规则引擎总是会对规则进行重新判定。例如,这种设置不会覆写Sequential规则集链接行为。

    Never,正如它的名称所暗示的,会关闭重新判定。有必要知道的是,如果判定的规则执行一个非空的操作,那么规则引擎只会考虑这个规则。例如,考虑一个具有Then操作的规则,但没有Else操作,就像我们已经定义的规则那样。如果规则被重新判定并且它的条件返回false,那么该规则就仍然是一个可用于再次判断的候选者,因为这条规则并没有执行任何操作。

9.3.2 规则引擎的寻迹(Tracing)和跟踪(Tracking

    给出真实世界中的一些规则集的复杂性和各种链接行为。我们将发现,看到在规则引擎中发生了什么是很有用的。正如我们在第6章讨论的那样,WF利用了.NET 2.0中的寻迹(Tracing)API,以及它自身内建的跟踪特性来提供仪器信息。在本节中,我们将使用规则引擎的寻迹和跟踪特性。涉及到第6章讨论的寻迹和跟踪的基本细节。

9.3.2.1 寻迹规则

    为了建立规则引擎的跟踪,我们需要一个带有寻迹开关设置的应用程序配置文件。下面的配置文件将记录所有来自于规则引擎的寻迹信息到WorkflowTrace.log。该文件将出现在应用程序的工作目录中。

image

    由寻迹信息提供的细节数量对于在我们的规则集中跟踪链接和逻辑问题是非常有用的。在本章中,我们使用的规则集将生成下面的寻迹信息(简单起见,忽略了一些寻迹信息)。

image

    寻迹的第一部分将提供关于依赖和副作用分析的信息。在分析的结尾,我们可以看到哪些操作将触发其他规则的判定。在寻迹的后面,我们可以看到规则引擎在执行我们的规则集时所采取的每一步措施。

image

    在寻迹中有大量的细节。我们可以看到每个条件判定的结果,以及引擎重新判定的哪些规则取决于副作用。当调试一个不含行为的规则集时,这些事实可以被证实为无价的。

    捕获这条信息的更正规机制是,使用一个跟踪服务,我们会在下一节介绍。虽然跟踪信息并没有寻迹信息那样详细,但是跟踪被设计为记录产品应用程序中的信息,而寻迹信息则适合于调试。

9.3.2.2 跟踪规则

    正如第6章介绍的那样,WF提供了可扩展的和可伸缩的跟踪特性来监控工作流执行。WF提供的一个跟踪服务是SQL Server跟踪服务,它把事件记录在SQL Server的表中。用于这个服务的默认跟踪信息记录了所有的工作流事件。

    为了支持跟踪,我们需要安装在SQL Server中的跟踪计划,以及应用程序配置文件来配置跟踪。下面的配置文件将添加跟踪服务到WF运行时,并在本地机器上指定WorkflowDB数据库。

image

    如果我们使用上述跟踪文件来运行bug打分的工作流,我们可以取出与规则相关的跟踪信息。下面的代码使用了已完成的实例ID,作为检索键来获取跟踪来自于SQL数据库的信息,也就是信息被持久化所在的地方。我们不需要修改工作流本身就可以使用跟踪。

private static void DunpRuleTrackingEvents(Guid instanceId)
{
    WorkflowRuntimeSection config;
    config = ConfigurationManager.GetSection("WorkflowWithTracking") 
        as WorkflowRuntimeSection;

    SqlTrackingQuery sqlTrackingQuery = new SqlTrackingQuery();
    sqlTrackingQuery.ConnectionString = config.CommonParameters["ConnectionString"].Value;

    SqlTrackingWorkflowInstance sqlTrackingWorkflowInstance;

    if (sqlTrackingQuery.TryGetWorkflow(instanceId, out sqlTrackingWorkflowInstance))
    {
        Console.WriteLine("{0, -10}{1, -22}{2, -17}",
            "Time", "Rule", "Condition Result");

        foreach (UserTrackingRecord userTrackingRecord in
            sqlTrackingWorkflowInstance.UserEvents)
        {
            RuleActionTrackingEvent ruleActionTrackingEvent =
                userTrackingRecord.UserData as RuleActionTrackingEvent;

            if (ruleActionTrackingEvent != null)
            {
                Console.WriteLine("{0, -12}{1, -25}{2, -17}",
                    userTrackingRecord.EventDateTime.ToShortDateString(),
                    ruleActionTrackingEvent.RuleName.ToString(),
                    ruleActionTrackingEvent.ConditionResult.ToString()
                    );
            }
        }
    }
}

    注意到,为了检索规则跟踪的事件,我们需要深入与UserTrackingRecord关联的用户数据中。上面的代码将生成下面的输出,它包括了每个规则判定的结果。

image

9.3.3 动态更新

    先前我们提到,使用声明性规则的一个好处是,我们可以在运行期间动态修改规则和规则集。如果在代码中指定了这些规则,那么每当我们想修改任意规则集时,我们必须重新编译并重新部署应用程序。使用WF,我们可以使用WorkflowChange类来修改工作流的实例。

    如果我们为下面的代码给出一个bug打分的工作流实例,那么它需要使用我们的工作流定义来实例化一个新的WorkflowChange对象。然后我们可以发现bug打分的规则是由名称通过RuleDefinitions实例来设置的。一旦我们设置了自己的规则,就可以对其进行修改。

private static void ModifyWorkflow(WorkflowInstance instance)
{
    Activity workflowDefinition = instance.GetWorkflowDefinition();

    WorkflowChanges workflowChanges;
    workflowChanges = new WorkflowChanges(workflowDefinition);

    CompositeActivity transient = workflowChanges.TransientWorkflow;
    RuleDefinitions ruleDefinitions =
        (RuleDefinitions)transient.GetValue(RuleDefinitions.RuleDefinitionsProperty);

    RuleSet ruleSet = ruleDefinitions.RuleSets["BugScoring"];
    foreach (Rule rule in ruleSet.Rules)
    {
        if (rule.Name == "AdjustBugPriorityForSecurity")
        {
            rule.Active = false;
        }

        if (rule.Name == "NotificationRule")
        {
            RuleExpressionCondition condition;
            condition = rule.Condition as RuleExpressionCondition;

            CodeBinaryOperatorExpression expression;
            expression = condition.Expression as CodeBinaryOperatorExpression;
            expression.Right = new CodePrimitiveExpression(120);
        }
    }

    instance.ApplyWorkflowChanges(workflowChanges);
}

    一旦我们设置了自己的规则,我们就可以在规则中迭代。在上面的代码中,我们关掉了AdjustBugPriorityForSecurity规则。我们可以通过转换规则的Activity属性来支持或禁用任意规则。

注意:对代码的修改将运用到工作流的一个特定的实例。换句话说,我们没有修改编译过的工作流定义。如果我们想要关闭安全规则,我们就必须在我们创建的每个bug打分工作流上运行这段代码,或者在设计器上修改规则集并重新编译。

    此外,上面的代码对我们的通知规则作出了更具动态性的修改。我们将规则的条件性表达式this.score > 75修改为this.score > 120。表达式可以被严格操作,但是记住.rules文件将包括CodeDom对象的一个XML表达式来生成这个规则。我们可以在这个文件中看到,是如何为NotificationRule创建条件的,如下所示:

image

    从XML中我们可以看到,我们需要替换CodePrimitiveExpression分配给CodeBinaryOperationExpression的Right属性。使用CodeDom类型,我们可以随意替换条件、修改操作,甚至创建新的规则。

9.4 小结

    在本章中,我们介绍了WF中的条件和规则。在WF特性的条件逻辑中有一些活动,包括强大的ConditionedActivityGroup。WF中Policy活动的意图是去执行一组规则。这些规则包括了声明性知识,我们可以为这些规则设置优先级,并使用前向链接执行语法。通过在声明性语句中而不是过程式编程中编写我们的企业级知识,我们就能得到极大的弹性。我们可以跟踪和寻迹规则,并动态更新规则集。WF是一个有能力的规则引擎。

posted @ 2009-03-25 00:49  包建强  Views(2673)  Comments(8Edit  收藏  举报