Drools 3 采用了原生的规则语言,那是一种非 XML 文本格式。在符号方面,这种格式是非常轻量的,并且通过“ expanders ”支持符合你问题域的 Domain Specific Language ( DSL )。这一章把焦点放在了 Drools 原生的规则格式。如果你想从技术上了解规则语言的机制,可以参考“ drl.g ”源文件,这是用 Antlr3 语法来描述规则语言。如果你使用 Rule Workbench ,内容助手将会为你完成大量的规则结构,例如输入“ ru ”,然后按 ctrl + space ,会为你建立规则结构。
一个规则文件通常是一个以 .drl 扩展名结尾的文件。在一个 drl 文件中,你可以有多条 rules , functions 等等。尽管如此,你也可以将你的规则分布在多个文件中,这有利于管理大量的规则。一个 DRL 文件是一个简单的文本文件。
一个规则结构大致如下:
rule " name "
ATTRIBUTES
when
LHS
then
RHS
end
可以看到,这是非常简单的。通常的标点符号都是不需要的,甚至连“ name ”的双引号都是不需要的。 ATTRIBUTES 是简单的,也是可选的,来提示规则的行为方式。 LHS 是规则的条件部分,需要按照一定的语法来写。 RHS 基本上是一个允许执行 Java 语法的代码的块(以后将会支持 groovy 和 C #)。任何在 LHS 中使用的变量都可以在 RHS 中使用。
注意:每行开始的空格是不重要的,除非在 DSL ( Domain Specific Language )语言中有特别的指明。
Domain Specific Language 是对原生规则语言的加强。它们使用“ expander ”机制。 Expander 机制是一种可扩展的 API 。你可以使用 .dsl 文件,来提供从域或自然语言到规则语言和你的域对象的映射。你可以将 .dsl 文件看成是对你的域模型的映射。 DSL 提供了更高的规则可读性,你可以选择使用你自己创建的 DSL ,或者是原生的规则语言。
在规则语言中存在一些保留字。你应该避免使用这些保留字,来命名规则文本中的域对象,属性,方法,功能。保留字如下: when , then , rule , end , contains , matches , and , or , modify , retract , assert , salience , function , query , exists , eval , agenda-group , no-loop , duration , -> , not , auto-focus 。
Figure 2.1. Single line comment
Figure 2.2. Multi line comment
一个包是 rule 和其他相关结构,像 import 和 global 的集合。 Package 的成员之间通常都是相关联的。一个 Package 代表了一个命名空间( namespace ),用来使给定的规则组之间保持唯一性。 Package 的名字本身就是命名空间,并且与文件或文件夹并无关联。
可以将来自不同规则源的规则装配在一起,前提是这些规则必须处在同一个命名空间中。尽管如此,一个通常的结构是将处于同一个命名空间中的所有规则都放在同一个相同的文件中。
下面的 rail-road 图显示了组成一个 Package 的所有组件。注意:一个 package 必须有一个命名空间,并且采用 Java 包名的约定。在一个规则文件中,各组件出现的位置是任意的,除了“ package ”和“ expander ”语句必须出现在任何一个规则之前,放在文件的顶部。在任何情况下,分号都是可选的。
Figure 3.1. package
Figure 3.2. import
Import 语句的使用很像 Java 中的 import 语句。你需要为你要在规则中使用的对象,指定完整的路径和类名。 Drools 自动从相同命名的 java 包中引入所需的类。
Figure 3.3. expander
expander 语句是可选的,是用来指定 Domain Specific Language 的配置(通常是一个 .dsl 文件)。这使得解析器可以理解用你自己的 DSL 语言所写的规则。
Figure 3.4. global
Global 就是全局变量。如果多个 package 声明了具有相同标识符的 global ,那么它们必需是相同的类型,并且所有的引用都是相同的。它们通常用来返回数据,比如 actions 的日志,或者为 rules 提供所需的数据或服务。 global 并不是通过 assert 动作放入 WorkingMemory 的,所有当 global 发生改变时,引擎将不会知道。所以, global 不能作为约束条件,除非它们的值是 final 的。将 global 错误的使用在约束条件中,会产生令人惊讶的错误结果。
注意: global 只是从你的 application 中传入 WorkingMemory 的对象的命名实例。这意味着你可以传入任何你想要的对象。你可以传入一个 service locator ,或者是一个 service 本身。
下面的例子中,有一个 EmailService 的实例。在你调用规则引擎的代码中,你有一个 EmailService 对象,然后把它放入 WorkingMemory 。在 DRL 文件中,你声明了一个类型为 EmailService 的 global ,然后将它命名为“ email ”,像这样: global EmailService email ;。然后在你的规则的 RHS 中,你可以使用它,像这样: email.sendSMS(number,message) 等等。
Figure 4.1. function
Function 是将代码放到你的规则源中的一种方法。它们只能做类似 Helper 类做的事(实际上编译器在背后帮你生成了 Helper 类)。在一个 rule 中使用 function 的主要优势是,你可以保持所有的逻辑都在一个地方,并且你可以根据需要来改变 function (这可能是好事也可能是坏事)。 Function 最有用的就是在规则的 RHS 调用 actions ,特别是当那个 action 需要反复调用的时候。
一个典型的 function 声明如下:
function String calcSomething(String arg) {
return " hola ! " ;
}
注意:“ function ”关键字的使用,它并不真正是 Java 的一部分。而 function 的参数就像是一个普通的 method (如果不需要参数就不用写)。返回类型也跟普通的 method 一样。在一条规则(在它的 RHS 中,或可能是一个 eval )中调用 function ,就像调用一个 method 一样,只需要 function 的名字,并传给它参数。
function 的替代品,可以使用一个 Helper 类中的静态方法: Foo.doSomething() ,或者以 global 的方式传入一个 Helper 类或服务的实例: foo.doSomething() ( foo 是一个命名的 global 变量)。
Figure 5.1. rule
Rule 结构是最重要的结构。 Rule 使用了形如“ IF ” something “ THEN ” action (当然,我们的关键字是“ when ”和“ then ”)的形式。
一个规则在一个 package 中必须要有唯一的名字。如果一个名字中含有空格,那就需要将名字放在双引号中(最好总是使用双引号)。
Attribute 是可选的(最好是每行只有一个 Attribute )。
规则的 LHS 跟在“ when ”关键字的后面(最好是另起一行),同样 RHS 要跟在“ then ”关键字后面(最好也另起一行)。规则以关键字“ end ”结束。规则不能嵌套。
Left Hand Side 其实就是规则的条件部分。 LHS 对应的 rail-road 图如下,我们在后面会做进一步解释:
Figure 5.2. Left Hand Side
Figure 5.3. pattern
Right Hand Side ( RHS )就是规则的结果( consequence )或者动作( action )部分。 RHS 的目的是 retract 或 add facts 到 WorkingMemory 中,还有针对你的 application 的动作。实际上, RHS 是当规则激发( fire )时执行的代码块。
在 RHS 中,你可以使用几个方便的 method 来改变 WorkingMemory :
“ modify(obj) ”:告诉引擎一个对象已经发生变化,规则必须重新匹配( obj 对象必须是出现在 LHS 中的对象);
“ assert(new Something()) ”:将一个新的 Something 对象加入 WorkingMemory ;
“ assertLogical(new Something()) ”:与 assert 方法类似。但是,当没有 fact 支持当前激发规则的真实性的时候,这个新对象会自动被 retract ,
“ retract(obj) ”:从 WorkingMemory 中移除一个对象。
这些方法都是宏指令,提供了到 KnowledgeHelper 实例的快捷方式(参考 KnowledgeHelper 接口)。 KnowledgeHelper 接口可以在 RHS 代码块中调用,通过变量“ drools ”。如果你在 assert 进引擎的 JavaBean 中加入“ Property Change Listener ”,在对象发生变化的时候,你就不用调用“ modify ”方法。
Figure 5.4. rule attributes
5.3.1 no-loop
默认值: false
类型: boolean
当在 rule 的 RHS 中修改了一个 fact ,这可能引起这个 rule 再次被 activate ,引起递归。将 no-loop 设为 true ,就可以防止这个 rule 的 Activation 的再次被创建。
默认值: 0
类型: int
每个 rule 都可以设置一个 salience 整数值,默认为 0 ,可以设为正整数或负整数。 Salience 是优先级的一种形式。当处于 Activation 队列中时,拥有高 salience 值的 rule 将具有更高的优先级。
默认值: MAIN
类型: String
Agenda group 允许用户对 Agenda 进行分组,以提供更多的执行控制。只有具有焦点的组中的 Activation 才会被激发( fire )。
默认值: false
类型: boolean
当一个规则被 activate (即 Activation 被创建了),如果这个 rule 的 auto-focus 值为 true 并且这个 rule 的 agenda-group 没有焦点,此时这个 Activation 会被给予焦点,允许这个 Activation 有 fire 的潜在可能。
默认值: N/A
类型: String
当处于同一个 activation-group 中的第一个 Activation fire 后,这个 activation-group 中其余剩下的 Activation 都不会被 fire 。
默认值:没有默认值
类型: long
Figure 5.5. Column
Example 5.1. Column
Cheese( )
Cheese( type == " stilton " , price < 10 )
一个 Column 由一个类的一个或多个域约束构成。第一个例子没有约束,它将匹配 WorkingMemory 中所有的 Cheese 实例。第二个例子对于一个 Cheese 对象有两个字面约束( Literal Constraints ),它们被用“,”号隔开,意味着“ and ”。
Figure 5.6. Bound Column
Example 5.2. Bound Column
cheapStilton : Cheese( type == " stilton " , price < 10 )
这个例子同前一个例子有点类似。但是在这个例子中,我们将一个变量绑定到匹配规则引擎的 Cheese 实例上。这意味着,你可以在另一个条件中使用 cheapStilton ,或者在 rule 的 RHS 中。
Field Constraints 使规则引擎可以从 WorkingMemory 中挑选出合适的 Fact 对象。一个 Fact 的“ Field ”必须符合 JavaBean 规范,提供了访问 field 的 getter 方法。你可以使用 field 的名字直接访问 field ,或者使用完整的方法名(省略括号)。
例如,以我们的 Chess 类为例,下面是等价的: Cheese(type = = …) 和 Cheese(getType = = …) 。这意味着,你可以使用不太严格遵守 JavaBean 规范对象。尽管如此,你要保证 accessor 方法是不带参数的,以保证它不会改变对象的状态。
注意:如果一个 field 使用原始类型( primitive type ), Drools 将会把它们自动装箱成相应的对象(即使你使用 java 1.4 ),但是在 java 1.4 下却不能自动拆箱。总的来说,尽量在 rule 所使用的类中,使用非原始类型的域。如果是使用 java 5 ,就可以比较随意了,因为编译器会帮你执行自动装拆箱。
Figure 5.7. Operators
有效的操作符是同域类型相关的。例如,对于日期域,“ < ”意味着“之前”。“ matches ”只适用于 String 域,“ contains ”和“ excludes ”只适用于 Collection 类型域。
最简单的域约束就是字面值约束,允许用户将一个 field 约束于一个已知值。
注意:你可以检查域是否为 null ,使用 = = 或 != 操作符和字面值' null ’关键字。如, Cheese(type != null) 。字面值约束,特别是“ = = ”操作符,提供了非常快的执行速度,因为可以使用散列法来提高性能。
Figure 5.8. Literal Constraints
Numeric
所有标准的 Java 数字基本类型都可以用。
有效操作符:
· ==
· !=
· >
· <
· >=
· <=
Example 5.3 . Numeric Literal Constraint
Cheese( quantity == 5 )
Date
当前只对“ dd-mm-yyyy ”的日期格式提供默认支持。你可以通过指定 drools.dateformat 系统属性,来改变默认的日期格式。如果需要更多的控制,要用谓词约束( Predicate Constraint )。
有效操作符:
· ==
· !=
· >
· <
· >=
· <=
Example 5.4. Date Literal Constraint
Cheese( bestBefore < " 27-Oct-2007 " )
String
可以使用任何有效的 Java String 。
有效操作符:
· ==
· !=
Example 5.5. String Literal Constraint
Cheese( type == " stilton " )
Boolean
只能用 “ true ”或“ false ”。 0 和 1 不能被识别,而且 Cheese(smelly) 也是不被允许的。
有效操作符:
· ==
· !=
Example 5.6 Boolean Literal Constraint
Cheese( smelly = = true )
Matches Operator
Matches 操作符后面可以跟任何有效的 Java 正则表达式。
Example 5.7. Regular Expression Constraint
Cheese( type matches " (Buffulo)?//S*Mozerella " )
Contains Operator and Excludes Operator
“ contains ”和“ excludes ”可以用来检查一个 Collection 域是否含有一个对象。
Example 5.8. Literal Cosntraints with Collections
CheeseCounter( cheeses contains " stilton " )
CheeseCounter( cheeses excludes " chedder " )
5.4.1.3 Bound Variable Constraint
可以将 Facts 和它们的 Fields 附给一个 Bound Variable ,然后在后续的 Field Constraints 中使用这些 Bound Variable 。一个 Bound Variable 被称为声明( Declaration )。 Declaration 并不能和“ matches ”操作符合用,但却可以和“ contains ”操作符合用。
Example 5.9. Bound Field using '==' operator
Person( likes : favouriteCheese )
Cheese( type == likes )
在上面的例子中,“ likes ”就是我们的 Bound Variable ,即 Declaration 。它被绑定到了任何正在匹配的 Person 实例的 favouriteCheese 域上,并且用来在下一个 Column 中约束 Cheese 的 type 域。可以使用所有有效的 Java 变量名,包括字符“ $ ”。“ $ ”经常可以帮助你区分 Declaration 和 field 。下面的例子将一个 Declaration 绑定到匹配的实例上,并且使用了“ contains ”操作符。注意: Declaratino 的第一个字符用了“ $ ”:
Example 5.10 Bound Fact using 'contains' operator
$stilton : Cheese( type == " stilton " )
Cheesery( cheeses contains $stilton )
5.4.1.4 Predicate Constraints
Figure 5.9. Predicate expression
Predicate 表达式可以使用任何有效的 Java 逻辑表达式。先前的 Bound Declaration 可以用在表达式中。
下面的例子将会找出所有男性比女性大 2 岁的 pairs of male/femal people :
Example 5.11. Predicate Constraints
Person( girlAge : age, sex = = " F " )
Person( boyAge : age -> ( girlAge.intValue() + 2 == boyAge.intValue() ), sex = = " M " )
5.4.1.5 Return Value Constraints
Figure 5.10. Return Value expression
一个 Retrurn Value 表达式可以使用任何有效的 Java 表达式,只要它返回一个对象,不能返回原始数据类型。如果返回值是原始数据类型,要先进行装箱。先前的 Bound Declaration 也可以使用在表达式中。
下面的例子跟上一节的例子一样,也将会找出所有男性比女性大 2 岁的 pairs of male/femal people 。注意:这里我们不用绑定 boyAge ,增加了可读性:
Example 5.12. Return Value Constraints
Person( girlAge : age, sex = = " F " )
Person( age = = ( new Integer(girlAge.intValue() + 2 ) ), sex = = " M " )
Conditional Elements 用来连接一个或多个 Columns 。
Figure 5.11. and
Example 5.13. And
Cheese( cheeseType : type ) && Person( favouriteCheese == cheeseType )
Cheese( cheeseType : type ) and Person( favouriteCheese == cheeseType )
Figure 5.12. or
Example 5.14. or
Person( sex == " f " , age > 60 ) || Person( sex == " m " , age > 65 )
Person( sex == " f " , age > 60 ) or Person( sex == " m " , age > 65 )
Figure 5.13. or with binding
Example 5.15. or with binding
pensioner : Person( sex == " f " , age > 60 ) || pensioner : Person( sex == " m " , age > 65 )
pensioner : ( Person( sex == " f " , age > 60 ) or Person( sex == " m " , age > 65 ) )
“ or ” Conditional Element 的使用会导致多条 rule 的产生,称为 sub rules 。上面的例子将在内部产生两条规则。这两条规则会在 WorkingMemory 中各自独立的工作,也就是它们都能进行 match , activate 和 fire 。当对一个“ or ” Conditional Element 使用变量绑定时,要特别小心,错误的使用将产生完全不可预期的结果。
可以将“ OR ” Conditional Element 理解成产生两条规则的快捷方式。因此可以很容易理解,当“ OR ” Conditional Element 两边都为真时,这样的一条规则将可能产生多个 activation 。
Figure 5.14 . eval
Eval is essentially a catch all which allows any semantic code (that returns a primitive boolean) to be executed. 在表达式中可以引用在 LHS 中出现的变量,和在 rule package 中的 Functions 。一个 Eval 应该是 LHS 中的最后一个 Conditional Element 。在一个 rule 中,你可以有多个 eval 。
Eval 不能被索引,因此不能像 Field Constraints 那样被优化。尽管如此,当 Functions 的返回值一直在变化时,应该使用 Eval ,因为这在 Field Constraints 中时不允许的。如果规则中的其他条件都匹配,一个 eval 每次都要被检查。(现在还不理解到底 eval 要怎么用?)
Example 5.16. eval
p1 : Parameter()
p2 : Parameter()
eval( p1.getList().containsKey(p2.getItem()) )
eval( isValid(p1, p2) ) // this is how you call a function in the LHS - a function called // "isValid"
Figure 5.15. not
“ not ”是一阶逻辑的存在量词( first order logic’s Existential Quantifier ) , 用来检查 WorkingMemory 中某对象的非存在性。现在,只有 Columns 可以放在 not 中,但是将来的版本会支持“ and ”和“ or ”。
Example 5.17. No Buses
not Bus()
Example 5.18. No red Buses
not Bus(color == " red " )
not ( Bus(color == " red " , number = = 42 ) ) // brackets are optional
Figure 5.16. exists
“ exists ” 是一阶逻辑的存在量词( first order logic’s Existential Quantifier ),用来检查 WorkingMemory 中某对象的存在性。可以将“ exists ”理解为“至少有一个”( at least one… )。它的意义不同于只有 Column 本身,“ Column ”本身可以理解为“对于每一个 … ”( for each of … )。如果你对一个 Column 使用了“ exists ”,那么规则将只 activate 一次,而不管 WorkingMeomry 中有多少个数据匹配了那个条件。
现在,只有 Columns 可以放在“ exists ”中,但是将来的版本会支持“ and ”和“ or ”。
Example 5.19. At least one Bus
exists Bus()
Example 5.20. At least one red Bus
exists Bus(color == " red " )
Figure 5.17. group
Group 的作用相当于代数学中的“()”,显式的指明操作的顺序。
Java 5 支持在原始类型与其对应包装类之间的装拆箱。尽管如此,因为要让 drools 能够在 J2SE 1.4 下运行,我们不能依靠 J2SE 。因此, drools 自己实现了自动装箱。被引用(即被 Bound Variable 绑定)的 Field 将被自动进行装箱(如果它们本身就是 object ,就不会有任何变化)。尽管如此,必须要注意的是,他们并不会被自动拆箱。
还有一点要注意的,就是对于 ReturnValue Constraints ,返回值的代码必须返回一个对象,而不能是一个原始类型。
Figure 6.1 . query
一个 query 只包含了一个 rule 的 LHS 结构(你不用指定“ when ”或“ then ”)。这是查询 WorkingMemory 中匹配条件的 Facts 的简单的方法。
要得到结果,要使用 WorkingMemory.getQueryResults(“name”) 方法,其中“ name ”就是 query 的名字。 Query 的名字在 RuleBase 中是全局的,所以, do not add queries of the same name to different packages for the same RuleBase 。
下面的例子创建了一个简单的 query 来查询所有年龄大于 30 的人:
Example 6.1. Query People over the age of 30
query "people over the age of 30"
person : Person( age > 30 )
end
我们通过一个标准的循环来迭代一个返回的QueryResults对象。每一次的iterate将返回一个QueryResult对象。我们可以用QueryResult对象的get()方法来访问每个Column,通过传入Bound Declaration或index position。
Example 6.2. Query People over the age of 30
QueryResults results = workingMemory.getQueryResults( "people over the age of 30" );
System.out.println( "we have " + results.size() + " people over the age of 30" );
System.out.println( "These people are are over 30:" );
for ( Iterator it = results.iterator; it.hasNext(); ) {
QueryResult result = ( QueryResult ) it.next();
Person person = ( Person ) result.get( "person" );
System.out.println( person.getName() + "/n" );
}