我们的代码和业务规则之间的交互主要是由我们定义的规则以及我们在运行规则引擎中输入的数据来完成的。为了与不在规则引擎上下文中的数据交互。Drools允许存在到我们代码的其他部分甚至是到其他的系统的各种各样的交互机制。其中一个最常用的工具叫做全局变量。
全局便令是在DRL代码中定义的,方式与在java代码中定义一个变量是一样的。要遵循的语法是全局关键字,然后是数据类型,然后是变量名:
glabal EShopConfigService configService;
全局变量可以是很多东西,比如外部服务,缓存数据的列表,规则配置的参数值,以及任何我们可以再java代码中定义的东西和我们希望从运行时获得一个可配置的组件。
在我们的DRL代码样例中(代码在 chapter-04-kjar中), 我们可以找到一个样例,它定义了一个服务,来配置我们的eShop,名字叫EShopConfigService,我们将会使用它演示;然而,如果我们用外部系统来替换它,例如像数据访问对象(DAO)或者Web Service,业务规则的互连可能性是无限的。下面讲解全局变量。
在前面的章节中我们已经看到了基础的业务规则结构:条件+操作。当规则中的一组特定条件被满足时,我们触发在该规则中定义的特定操作。到目前为止,这些操作只是对Java bean或系统日志进行了基本的修改。但是Drools规则能做的可不止这点:
当一个规则变得太复杂或者它由多个复杂的条件组成,在一个规则中定义它们可能不是最好的方法。在命令式编程中(java或者c++),我们可以分解一个复杂的方法或者函数,把它变为许多小的简单的方法。在Drools中,我们应该遵循一个类似的结构,定义多个、更简单的规则一起工作。
DRL遵循声明式编程,也是由于这个特性,我们不能从另一个规则中调用一个规则。因此,这种分裂必须以不同的方式进行。为了把我们的规则拆成简单的规则,规则的操作需要添加或修改数据,以便其他规则将对数据进行重新评估,并查看是否应该触发它们的操作。
在我们规则语句中的then语句,规则可以推断出新的信息。更多的数据可以提供给工作中的内存,以便对使用insert关键字的所有规则进行进一步的评估,如下:
rule "Classify Customer - Gold"
when
$c: Customer( category == Customer.Category.GOLD )
then
insert(new IsGoldCustomer($c));
end
rule "Classify Item - Low price"
when
$i: Item(cost < 10.00)
then
insert(new IsLowRangeItem($i));
end
rule "Suggest gift"
when
IsGoldCustomer($c: customer)
IsLowRangeItem($i: item)
then
System.out.println("Suggest giving a gift of item "+$i.getName()+" to customer "+$c.getName());
end
上面的例子出自:chapter-04-kjar/src/main/resources/chapter04/workingMemoryModif/classify-item-rules.drl,我们可以看到一个简单的插入和分隔一个规则为多个小规则的样例。我们可以检查一个客户是否有一个黄金类别,我们有一个低成本的产品,并且在相同的规则下对礼品产品提出建议。然而我们将规则拆分为三部分,插入新的模型元素( IsGoldCustomer 和IsLowRangeItem),让另一个规则基于这两个元素来决定我们的主要决策。
通过将规则分解为更小的部分,我们有一些规则来决定什么是黄金客户,以及如何处理这些问题,我们可以用许多不同的方式来定义一个黄金客户。之后,所有的规则需要依赖于 IsGoldCustomer事实和任何额外的条件来决定应该做什么。
我们还可以使用已经存在的数据,这些数据可能触发了规则的条件,并通知引擎应该重新评估它。这可以使用miodify关键字来做到,就像下面这样:
rule "Categorize Customer - Gold"
when
$c: Customer( category == Customer.Category.NA )
$o: Order(customer == $c, orderLines.size() > 10)
then
modify($c) { setCategory(Customer.Category.GOLD); }
end
该规则修改客户对象,将其设置为gold类别.在修改之后,引擎将会被通知这个对象需要对引擎中的所有规则进行重新评估。这意味着,如果我们将一个没有类别的客户和一个订单的名称加上超过10个条目,它不仅会设置相应的类别,还会触发任何依赖于此条件的规则。
另一个可以替换modify的关键字是update。update关键字不将代码块作为修改关键字。相反,它只接收一个变量,这样就可以预先对变量进行修改。下面的代码示例将替换前面代码部分中的modify关键字,如下所列:
$c.setCategory(Customer.Category.GOLD);
update($c);
但是,不鼓励使用update更新,更好的方式使使用modify块,因为它们允许对事实的执行进行语法分组,从而触发规则的重新评估。
正如你所看到的,工作内存的修改对于将规则分解为多个规则是非常重要的,因为所有的规则都将首先被触发,这是在引擎可用的数据的基础上进行的。通过修改数据,引擎可以继续触发规则,直到不再有规则匹配可用的数据。如果我们不modify/update数据,引擎将无法看到您可能在一个事实中所做的更改,而这个对象将不会触发更改之前已经匹配的规则的任何一个规则。
另外,我们可能会让引擎知道它应该重新评估规则,因为在工作内存中存在的一个数据元素已经不在了。这可能会取消掉未来的规则,这些规则还没有被评估,或者触发其他规则,如下:
rule "Init current date"
when
then insert(new Date());
end
rule "Expire coupons"
when
$now: Date()
$cp: Coupon( validUntil before $now )
then
delete($cp);
end
rule "Execute coupon"
when
$o: Order()
$cp: Coupon(order == $o)
then
System.out.println("We have a coupon for this order!");
end
在之前的规则中,我们首先确保我们有一个当前时间的变量,以便为了与Init current date规则进行比较*(我们可以看见他的条件是空的,也就是直接置为True)。然后,如果在内存中你有一个过期的优惠券,那么第二个规则将会移除它。即使我们添加了一个与这个优惠券相关联的订单,第二个规则也不会被触发,因为即使匹配,第一个规则的执行将取消它。
delete和retract关键字都是有效的,可以从工作内存中删除对象,但是retract已经过时了。它们的语法是等价的,它们在DRL代码中是相互交换的。
如您所见,更改规则中的工作内存意味着,当我们调用在KieSession中fireAllRules,不仅符合当时数据的规则将被触发,而且新规则可能在规则执行期间被触发、触发或取消。
这是一个非常强大的工具,因为它允许我们去控制规则的执行而不用明确的执行他们和破坏他们,没有任何的控制,它会很容易导致无限循环。我们将来看这个问题并来避免这个问题。
Drools规则是数据驱动的。这意味着唯一的一种实现规则的方式就是将数据添加到与该规则匹配的引擎中。但是,有很多情况下,我们需要让匹配某些情况的规则被筛选出来。其中一个过滤机制称为规则属性。
规则属性是我们添加到规则中的额外特性,让我们可以以特定的方式修改它们的行为。这里有许多规则特性(我们将解释最常用的和它们可能的组合),每一个都修改了我们以不同的方式过滤规则执行的方式,就像下面这样:
rule "simple attribute example"
enabled false
when Customer()
then System.out.println("we have a customer");
end
如果enabled属性设置为false,这个规则就会被评估,然而,它却不会被执行。这可能是这里显示的规则属性的最简单的例子,它可以看到规则属性是在规则的某个部分之前编写的。然而,即使这是他们的语法位置,他们的执行顺序也会在规则的条件之后。这意味着如果在工作内存中并没有为规则的条件找到匹配的数据,那么规则属性也都不会执行。然而,当规则集与工作内存中的一组对象数据匹配时,规则属性将作为一个额外的组件来决定该匹配是否应该现在、稍后或根本不触发。
下面的小节将展示一些最常用的规则属性,以及它们如何影响规则的执行,如下所示:
当我们定义规则的时候,首先将数据与它的条件相匹配的规则将是规则列表中的第一个规则。这意味着规则执行的顺序不是决定性的。然而,有时候,我们可能需要一些规则优先于其他的规则先执行。
例如,在我们的例子中,关于规则的分类,在之前的小节中,我们可能看到这样一种情况,我们可能有一个特定的子范畴在中间范围内的一组特殊值中,我们可能想要的是这个规则找到匹配的情况比一般的中端分类更优先,就像下面这样:
rule "Classify Item - Mid Range"
when $i: Item(
cost > 200 && cost < 500,
category == Category.NA )
then
$i.setCategory(Item.Category.MID_RANGE);
update($i);
end
rule "Classify Item - Mid/High Range (special)"
when
$i: Item( cost > 300 && cost < 400,
category == Category.NA )
then
$i.setCategory(
Item.Category.SPECIAL_MIDHIGH_RANGE);
update($i);
end
在这个样例中,如果我们添加一个项目,成本是350,第一个规则将会先于第二个被评估,如果我们想让第二条规则优先,我们可以使用salience规则属性。,为它设定一个更高的优先级。salience越高,规则的优先级越高,如下所示:
rule "Classify Item - Mid/High Range (special)"
salience 10
when
$i: Item( cost > 300 && cost < 400 )
then
$i.setCategory(
Item.Category.SPECIAL_MIDHIGH_RANGE);
update($i);
end
默认情况下,所有规则都有一个隐式的salience属性,值为0,您可以为salience属性分配正或负的值,以便在规则的其余部分执行之前或之后执行它们。请重视的是,规则属性的评估将会是在规则条件与工作内存中的一组数据相匹配了之后才进行的,因此,如果该规则不使用现有的数据触发,那么它也不会被触发,不管它的值有多高或多低。
PS:注意,在Drools 6中,规则在缺省情况下具有隐式的相对重要性,它优先考虑在相同的DRL文件中出现的规则。然而,在不同的DRL文件中,规则之间并没有相对隐式的显著性。
在我们重写的规则中有一个问题,我们不再检查被设置为NA的类别属性。我们这样做是为了在开始使用Drools规则时解释一个常见的问题。正如您所看到的,规则执行的的结果是更新项目并为其设置一个类别。一旦update被调用了。那么这个对象将在所有规则下,被重新评估,并且如果它仍然符合其条件(例如成本介于300到400之间),它将多次触发该规则。
这种无限循环可以以不同的方式进行管理。在早期版本中,我们检查在规则的条件下,这个类别仍然是NA。一旦我们修改了类别,更新对象就会触发对规则的重新评估,然而,因为它不再有一个NA类别,它也就不会符合规则条件了。在可能的情况下,这可能是最好的方式了,但是如果检查这种类型的条件是复杂的,规则属性的存在是为了让引擎知道一个特定的规则在修改工作内存后不应该重新评估它自己。属性是不会循环的属性,如下所示:
rule "Classify Item - Mid/High Range (special)"
no-loop
salience 10
when
$i: Item( cost > 300 && cost < 400 )
then
$i.setCategory(
Item.Category.SPECIAL_MIDHIGH_RANGE);
update($i);
end
鼓励你测试以下,当这个属性被删除时到底发生了什么。无限循环是通过这个属性停止的,因为这是一个反复触发自己的规则。
另一个非常简单的规则属性是enabled规则属性。它接收一个布尔参数,让引擎知道是否应该执行该规则。如果是假的,规则不会被评估,如下:
rule "Classify Item - Mid/High Range (special)"
enabled true
no-loop
salience 10
when
$i: Item( cost > 300 && cost < 400 )
then
$i.setCategory(
Item.Category.SPECIAL_MIDHIGH_RANGE
);
update($i);
end
这似乎是一个奇怪的规则属性。为什么我们要使用规则属性来禁用规则呢?你可以把它注释掉或者删除规则。为了理解它的存在,我们需要理解规则属性,即使它们是在规则条件之前写的,但是确实在规则条件之后才进行评估。这意味着它们可以使用来自条件的数据来决定启用属性的布尔值、salience属性的整数值,或者我们将来可能定义的任何其他属性值。
有了这些信息,我们将用两个不同的规则属性的变量值来重写我们的规则,我们将根据条件的项目成本来设置salience值,我们将根据一个全局变量的布尔方法来设置这个规则是否启用,如下:
global EShopConfigService configService;
...
rule "Classify Item - Mid/High Range (special)"
no-loop
salience ($i.getCost())
enabled(configService.isMidHighCategoryEnabled())
when
$i: Item( cost > 300 && cost < 400 )
then
$i.setCategory(
Item.Category.SPECIAL_MIDHIGH_RANGE);
update($i);
end
正如您在前面的例子中所看到的,我们定义了基于变量数值的规则的salience优先级(具体来说,在条件下检测到的物品的成本值),我们在全局变量中基于布尔方法的返回值设置启用属性。只要条件是用括号和Java代码编写的,引擎就能够理解它们。
现在我们已经了解了规则属性的结构并对最简单的三个属性进行了一些简单的解释,现在是时候通过更复杂的规则属性来继续加强我们的规则游戏了。下一个我们将要看到的集合是用来定义规则分组的。
我们确切地说,哪一个规则将会被触发,这种方式不应该是对规则进行微控制的方式,然而,这并不意味着我们应该让引擎运行我们在同一时间定义的所有规则。即使我们创建了一个中等规模的基于规则的项目,我们也会看到我们的规则有不同的类别。在我们的eShop示例中,一些规则将用于数据输入验证,一些规则将验证应用于现有购买的促销,一些将对我们的购买发票应用不同的税,等等。每个规则在不同的时间被应用都是有意义的。Drools为我们的规则提供分组机制,使其能够每次都激活一组规则。这些组也通过规则属性来定义。
这似乎与声明式方法不同,然而,它仍然受控于我们为规则引擎所提供的数据。那么控制一条规则与控制一组规则的根本区别就是,这个组也被规则引擎所控制。声明式方法仍然适用,但只适用于我们所处的所有规则的子集。
最常用的规则分组类型是使用agenda-group规则属性来定义的。这个规则属性定义了一个键,并且可以被KieSession中的代码激活,并且可以根据需要修改多次来适应你的场景,如下:
rule "Promotion: more than 10 pencils get 10% discount"
agenda-group "promotions"
when
OrderLine(item.name == "pencil", quantity > 10)
then
insert(new Discount(0.10));
end
前面的规则定义了“promotions(促销)”分组下的规则.这个特定的组应该有所有的规则,包括将促销活动应用到购物车中,并且,如果有的话,要确定接下来应该触发的规则,仍然是规则引擎的工作
agenda组通过对kiesssion对象的API调用来手动激活(在调用fireAllRules之前),如下:
KieSession ksession = …;
ksession.getAgenda().getAgendaGroup("promotions").
setFocus();
ksession.fireAllRules();
值得一提的是,默认下,所有的规则都有一个隐式的MAIN agenda组。在默认情况下,kiesssion会激活这个组,而不定义agenda-group属性的那些规则,都属于这个组。
而且,每条规则,不管它是否在激活的agenda组中,当规则评估被触发时候都会被评估。激活的agenda组将确定在规则执行时应该执行的规则匹配组。
还值得一提的是,当一个规则被触发时,它还可以定义将通过隐含的全局变量kcontext来激活其他agenda组,如下:
rule "Done with promotions. Onward with printing invoice"
salience -100 //last rule of group to run
agenda-group "promotions"
when
OrderLine()
then
kcontext.getKnowledgeRuntime().getAgenda().
getAgendaGroup("MAIN").setFocus();
end
仔细看看前面的规则。这里有很多技巧。首先,你你在salience属性中有一个负值。这意味着这个规则将会以非常低的优先级,因此即使它被激活,只要有其他规则匹配,它将优先于这个。这使得该规则成为可能运行的组的最后一条规则。条件要求一个OrderLine对象,因此,只要我们有一个OrderLine,并且规则已经完成了它们所需要的一切,我们就会执行这个规则的操作。
作为这条规则的效果 ,kcontext是作为一个访问KieSession的变量,方法是使用 getKnowledgeRuntime()。通过这一点,它可以激活下一个agenda组,就像它在普通Java代码中所做的那样.你可以看到激活的agenda组是MAIN agenda组,他是默认的组,所以下一组将会被匹配并可能被触发的规则那些没有设置agenda-group的规则所组成的MAIN组。
agenda组非常有用,它不仅定义了我们规则中的特定序列,还让规则决定下一组被激活的规则。非常复杂的情况是。规则必须要控制的非常复杂的情况下,接下来要遵循的一组操作可以用这些规则来表示。然而,有时,规则通过用户友好的编辑器向业务用户公开,他们通常需要一种更图形化的方式来定义规则组序列。
有一个用于业务流程管理(BPM)的工具,称为jBPM,它使用Drools作为基本API和规则引擎。它允许最后的用户去定义图标的方式来展示进程的步骤的顺序。其中的一些步骤可以是规则执行,并确定应该在特定点触发的规则,他们在在流程中的规则步骤和将要调用的规则之间使用一个公共的属性。ruleflow-group规则属性
Ruleflow组用于与业务流程的唯一交互,并且没有一个公开的API来调用应该被激活的ruleflow组。相反,这是由jBPM的运行时直接完成的.
分组被用于将规则划分进组内;然而,有时候,我们需要我们需要这些群体有更具体的行为。例如,我们可能定义一个特定的规则的组应该是相互排斥的。要想定义这样的行为,Drools定义了一个规则属性叫activation-group,这就定义了只有一个规则应该在那个组中触发。如果我们提供的数据在同一个激活组中匹配5个规则,那么第一个触发的数据将会取消其他4个规则。
PS:注意,规则属性只能定义一次规则,然而,可以在单个规则中定义多个规则属性。一旦我们理解了所有属性类型的用途,规则属性的组合是一个非常强大的工具。
有时我们希望我们的规则只在特定的时刻被考虑。一些规则,特别是公司的政策,比如零售的特别折扣,税收 计算,以及假日特价,只在特定的日期才有意义。有一些规则属性,有这个目的,它们允许您控制某个规则是否在某个特定时间点被启用。
这些属性中的两个,date-effective和date-expires,决定指定要启用的特定规则的开始和结束日期。如果我们假设政府在2015年到2020年的每一次购买中都要增加一项具体的税收,这将是一个不错的选择来定义这条规则的方法如下:
rule "Add special tax of 3%"
date-effective "01-Jan-2015"
date-expires "31-Dec-2020"
when $i: Item()
then $i.setSalePrice($i.getSalePrice() * 1.03);
end
默认情况下,支持dd-mmm-yyy日期格式。您可以通过提供一个替代日期格式掩码作为 drools.dateformat系统属性来定制它。
还有另一种常见的情况,我们可能需要定期将规则从启用到禁用,或者基于特定的日期配置。在我们的eShop中,这可能与促销活动有关,比如在周六购买啤酒买一送一。这条规则不适用于任何一天,因此,我们谁用特性的规则属性:calendars,来指定什么时候规则需要变为可用。
当然,只要规则的条件继续匹配特定的数据,我们可能需要在特定的调度上重新触发特定的规则。对于这种情况,这里有一个timer规则属性,它允许你去设置一个cron表达式或者是一个基于区间的规则。
下面是这两种属性在一起工作的一个例子:
rule "Weekday notifications of pending orders"
calendars "weekdays"
timer (int:0 1h)
when Order($id: orderId)
then emailService.sendEmail("Pending order: "+$id);
end
rule "Weekend notifications of pending orders"
calendars "weekends"
timer (cron:0 0 0/8 * * ?)
when Order($id: orderId)
then emailService.sendEmail("Pending order: "+$id);
end
正如您可以看到的,这两个规则几乎是一样的。如果在工作内存中有一个订单,他们会通过一个名为emailService的全局变量的助手类发送一封电子邮件。这两个规则之间的主要区别是由规则属性提供的。第一个规则只会在工作日的日历上激活,而第二个规则只会在周末的日历上激活。然而,只要条件仍然满足,每条规则都会以不同的速度触发,第一条规则将每隔一小时触发一次(第一次的时候是零延迟的),第二个规则将会现在00:00,08:00,16:00点准确的触发。它们都要求规则执行被调用,这样就可以不断调用fireAllRules(),以使触发率达到目的。这些日历必须通过使用getCalendars方法来进行配置,如下所列:
ksession.getCalendars().set("weekday", new Calendar() {
//for simplicity, any date/time matches this calendar
public void isTimeIncluded(long timestamp) {
return true;
}
});
测试代码是TimersAndCalendarsTest。任何与 org.kie.api.time.Calendar接口相匹配的对象都可以定义任何形式的日历,并使用任何形式的业务逻辑。
到目前为止,我们已经看到了一些方法来管理我们的规则以触发新规则的调用。这将极大地帮助我们,以便能够将我们的规则分割成在后台通过内存中的数据进行交互的简单组件 。尽管它很强大,但它也会给我们带来一些额外的并发症,比如规则的执行次数比我们期望的要多。幸运的是,Drools从我们定义它们的语法为我们提供一组元素来控制规则的执行。
第一个也是最简单的例子,我们可以进入无限规则执行循环,当规则修改工作内存时,它会重新触发自己。让我们来看看下面这个问题的一个例子:
rule "Apply 10% discount on notepads"
when $i: Item(name == "notepad", $sp: salePrice)
then modify($i) { setSalePrice($sp * 0.9); }
end
在这条规则中,我们的意图是降低我们库存中记事本的销售价格。 我们只是不改变项目的价值,但是,我们也想通知引擎它改变了。如前所述,这是使用modify关键字完成的 ,我们这样做,因为我们可能有其他的规则,需要重新评估商品的价格,因为价格是不同的。
问题是,如果修改后的对象仍然与该规则的条件匹配(并且它的名称仍然是记事本),那么它也会重新评估自身。这将导致对同一元素执行大量的规则执行。
避免这种不需要的循环的方法是一个非常简单的属性,不需要惊讶,no-loop。no-loop规则属性防止规则重新启动,不考虑规则对工作内存的更改。它的语法非常简单,如下面的例子所描述的:
rule "Apply 10% discount on notepads BUT ONLY ONCE"
no-loop true
when $i: Item(name == "notepad", $sp: salePrice)
then modify($i) { setSalePrice($sp * 0.9); }
end
true条件是选填的,你可以只写no-loop就够了。这里写了boolean值为true的原因是正如我们前面提到的,它可以是一个变量,它决定了这个规则是否被设置为没有循环。
值得一提的是, no-loop只会阻止对于同样的数据的规则的重新启动 ,如果这是最后一条需要触发的规则的话。如果另一个规则改变工作记忆以与此规则相匹配的方式,无no-loop条件不会阻止它第二次执行。有时,这是一种需要的行为,然而,如果不是这样,我们还需要讨论其他类型的循环预防策略。
让我们来看一个例子:
rule "Give extra 2% discount for orders larger than 15 items"
no-loop true
when $o: Order(totalItems > 15)
then modify ($o) { increaseDiscount(0.02); }
end
rule "Give extra 2% discount for orders larger than $100"
no-loop true
when $o: Order(total > 100.00)
then modify ($o) { increaseDiscount(0.02); }
end
这些规则有no-loop属性,所以即使他们修改order对象,他们也不会触发自己规则。然而,没有什么能阻止它们相互激活。因此,第一个规则可以触发第二个规则,那么第二个也是可以触发第一个规则的,然后就循环起来了。这种类型的无限循环需要一些比no-loop属性更强的东西。
确保规则不会被相同对象重新触发的一种快速方法是在这些麻烦的规则中添加一个名为“lock-on-active”的属性。无论一个ruleflow组变为激活或者一个agenda组获得了焦点(focus方法),任何在这个组内的规则,一旦他们的lock-onactive被设置为true了,那么他们就不会再被相同的对象置为激活。不管更新的起源是什么,一个匹配规则的激活都会被丢弃。下面是使用lock-on-active规则重写的规则的一个示例:
rule "Give extra 2% discount for orders larger than $100"
lock-on-active true
when $o: Order(total > 100.00)
then modify ($o) { increaseDiscount(0.02); }
end
在第二种情况下,规则只会触发同一对象的一次。你可以在 LoopingExamplesTest中找到测试类。
这是一个更强的无循环版本,因为现在的变化不仅仅是由规则本身。它是计算规则的理想选择,在这里有许多规则可以修改一个事实,并且不希望任何规则重新匹配和再次触发。只有当ruleflow-group不再激活,或者agenda组失去焦点;这些lock-on-active属性设置为了true的规则,同一对象将可以再次匹配成功,即还是可以重复循环。这也说明了lock-on-active只对激活或者获得焦点的情况有用~~
no-loop属性和lock-on-active属性在控制不需要执行的循环时,给我们很大的力量。然而,这些属性也带来的问题,这是因为,在条件中使用的所有对象都不会重新触发规则,不管它们被相同或其他规则修改了多少。在很多情况下,模型是复杂的或难以更改的,这可能不是我们想要的行为,如果它们发生在特定的属性中,我们可能仍然需要重新评估某些更改。
这种细粒度的控制是可能的,最简单的方式就是,通过向存储我们想要检查的条件的对象添加标记属性。一些规则将更改这些标志,而其他一些规则将对它们进行检查,以确定它们是否应该或不应该再次评估。下面是一个例子:
rule "Add 2% discount for orders larger than $100"
when $o:
Order(total > 100.00, has100DollarsDiscount == false)
then
modify($o){
increaseDiscount(0.02);
setHas100DollarsDiscount(true);
}
end
rule "Add 2% discount for orders larger than 15 items"
when $o:
Order(total > 100.00, has15ItemsDiscount == false)
then
modify($o){
increaseDiscount(0.02);
setHas15ItemsDiscount(true);
}
end
在上面的规则中,不需要将规则属性添加到规则中,因为使用两个标志属性已经标记了该对象不被再次评估。如果在工作内存中对对象进行了其他修改,则会重新触发该规则。
这种解决方案有两个主要问题。一个问题是,我们最终会使用额外的属性,而不是与模型的实际内容有直接关系,但是,它们与规则的执行更相关。第二个问题是,当我们有这么多规则时,我们需要检查太多的标志,以便使规则易于理解。请记住我们写规则的主要目标:保持他们自治独立,并尽可能的简单(原子性质)。如果你有了太多的涉及到具体的规则的标记属性,那么自治性就会被打破。
不要担心,Drools中还有更多的技巧可以帮助我们解决这些问题。
Drools规则条件是基于Java类型构建的.这意味着我们需要有一组定义好的Java类来定义我们的模型并从我们的规则中使用它。我们之前的所有示例都是基于Java类的存在来表示订单、折扣、客户等等。
这不是Drools必须定义数据模型的唯一方法。在DRL文件内,也就是我们定义规则的地方,我们还可以定义将要创建,编译的新类型,它们在运行时,与规则同时可用,并且可以很容易的被修改。这些被称为声明类型,并且他们在定义只对特定的规则组有意义的数据模型上扮演着很重要的部分(就像接口对象,这些对象不一定是规则输出结果的一部分),在DRL结构中,他们定义在规则之前。
declare SpecialOrder extends Order
whatsSoSpecialAboutIt: String
order: Order
applicableDiscount: Discount
end
前面的例子包含了一些值得提及的内容,例如:
这些类型可以从规则的条件和结果中使用,就像其他Java类一样。你可以访问这些属性的getter/setter方法,这些工作都被规则引擎在编译的时候自动办了。唯一的区别在于试图从普通Java代码的规则之外访问这些对象。但是,这是有可能做到的,它需要使用一个可以通过KieBase访问的反射API,如下:
KieSession ksession = ...; //previously initialized
FactType type = ksession.getKieBase().getFactType(
"chapter04.declaredTypes", "SpecialOrder");
Object instance = type.newInstance();
type.set(instance, "relevance", 2L);
Object attr = type.get(instance, "relevance");
上面你可以看到,在java代码中使用我们声明的类型是很复杂的。所以使用这种声明类型的方式的场景是:仅仅在规则代码使用我们声明的类型,在我们的java代码中基本不使用的情况下,我们可以考虑使用这种方式。我们使用这种方式的样例放在DeclaredTypesTest类里,地址在chapter-04/chapter-04-tests项目。
不论我们什么时候在规则的结果操作区域使用modify或者update关键字,我们将通知引擎,过滤类似的对象类型的规则应该再次对对象进行重新评估。这个重新评估,默认的发生在整个对象上。只要对象的一个属性发生变化,规则就会将其视为匹配的新对象。
这可能会导致一些问题,因为我们不希望对某些更改进行重新评估。在这种情况下,no-loop和lock-on-active,可能会有所帮助。然而,如果我们希望规则仅控制某些属性的变化,我们需要编写非常复杂的条件。而且,如果未来,模型在大型规则基础上发生了变化,那么您可能不得不修改许多规则,以避免不期望的规则重新执行。
幸运的是,Drools提供了一个引擎解决这个问题的特性。它允许规则编写人员定义bean的属性,如果它们在工作内存中被更新,就应该对它们进行监视。在个特性是在我们的规则中使用的数据模型(可以使java的,也可以是我们的声明类型)中定义的,我们称呼他为property-reactive类。
为了实现这个特性我们首先需要去标记我们需要在规则中使用的类型,标记的方式使通过Property Reactive的注解来做,你可以类比下我们使用框架时候的注解。这个注解允许引擎知道,只要将该类型的对象添加到工作内存中,就需要对其更改进行特殊的过滤。这个注解可以被添加进java的类(在class级别)或者我们的声明类型(在第一行之后,即定义类型名之后),例如:
declare PropertyReactiveOrder
@propertyReactive
discount: Discount
totalItems: Integer
total: Double
end
在我们的声明类型被“标记为”一个property-reactive bean后,我们可以让我们的规则来定义这些bean的那些属性是需要被监控变化的,那些是不需要被监控的。
为此,我们使用@watch注释,在规则的每个特定条件之后,该规则应该具有这种类型的过滤应用,如下所示:
rule "Larger than 20 items orders are special orders"
when
$o: PropertyReactiveOrder(
totalItems > 20
) @Watch (!discount)
then
modify ($o) {
setDiscount(new Discount(0.05));
}
end
在上面的规则中,@Watch注解使得规则的行为与添加了一个no-loop或者是lock-on-active注解是一样的。它会避免这个以及其他的规则去为了这个相同的元素而进行重新触发。但是,如果任何规则以某种方式修改对象,而不是改变它的折扣,它就不会避免这个规则的重新触发。这是property-reactive beans的主要功能;您可以确定哪些属性更改可以重新触发规则,哪些不能。
@watch注释可以在规则中使用,用于监视property-reactive bean属性的不同情况。如果不存在,则根据条件结构,默认情况下可以推断出监视字段。这里,我们看一个例子:
Property reactive beans的对象改变了,只能使用modify关键字,通知规则引擎。update关键字无法区分正在更改的bean的属性之间的区别。这也是不推荐使用update关键字的原因之一。
到目前为止我们所见,我们在规则中使用的数据模型是如何可塑和可扩展的。基于其修改和规则定义的条件,我们可以以非常简单的方式创建非常复杂的环境。接下来我们需要学习的是如何用一种简单而又强大的方式来定义规则中的条件。让我们来看看一些最常用的操作,我们可以用它们来比较我们的规则定义中的数据。
到目前为止,我们已经看到了在规则中检查属性值的最简单的情况。我们还讨论了规则可以管理数据更新的所有不同方式,这可能会重新触发检查我们的规则。然而,规则的力量并不仅限于此,因为有许多不同的方法可以编写规则条件,这将允许我们创建既强大又简单的规则。
Drools已经提供了一组操作,您可以使用这些操作来比较不同的对象。这些对象可能直接生活在工作内存、全局变量、文本值或这些类型的任何组合中。我们将列举下面几节中最常用的部分,将它们分为以下几部分:
在之前的规则中,我们已经看到了这些操作的一些示例,因为对于任何有编程经验的人来说,这些操作是最容易理解的。布尔运算是指使用AND,OR,XOR等等。数字操作是比较两个数值的。下面是一些针对项目类型属性的操作的示例:
Item(
salePrice > 100.00 && salePrice <= 500.00
&& salePrice != 101.00
)
在前面的例子中.我们检查了我们的Item对象的salePrice属性是否值大于100,小于或等于500,不同于101。
其中一些操作是Drools的固有特性,比如使用AND操作。在前面的例子中,使用&&方法是不需要的,因为用逗号分隔的任何比较都被认为是一个必须检查的新条件。然而,需要布尔条件来表示两种情况中的一种情况需要为真(这里是一个OR布尔表达式)。
使用布尔表达式是可能的,但是如果需要的话,它们通常会标记出我们的规则的设计需要修改的地方。这是因为AND表达式是语言固有的,而且使用OR表达式的规则应该被视为同一规则中的多个条件(因此,非原子性)。每当需要一个或表达式来表达规则时,请考虑将规则分解为两个不同的规则,如下面的例子:
rule "Add 5% discount for minors or seniors"
when $o: Order(customer.age < 18 || customer.age > 60)
then $o.increaseDiscount(0.05);
end
这可以分为两个规则:
rule "Add 5% discount for minors"
when $o: Order(customer.age < 18)
then $o.increaseDiscount(0.05);
end
rule "Add 5% discount for seniors"
when $o: Order(customer.age > 60)
then $o.increaseDiscount(0.05);
end
布尔表达式似乎是尽可能避免的,但是,在本章后面的其他操作中,我们将看到它们所使用的其他一些用法。目前,我们只需要记住它们是存在的,并且只有在真正需要的时候才尝试使用它们。
###1.4.2 Regex操作--------匹配
匹配是一个操作符,我们可以使用基于字符串的对象和属性。它允许我们检查它们是否遵循特定的正则表达式。最常见的用法是检查字符串是否表示有效数字、电子邮件或需要验证的任何特殊字符顺序。正则表达式是一个非常复杂的主题,您可以在网上了解到更多。在规则条件下,我们将看到一个小的例子,通过创建一个规则来检查我们的客户是否有一个有效的电子邮件地址,如下:
rule "validate customer emails"
when $c: Customer(email not matches
"[A-Za-z0-9-.]+@[A-Za-z0-9-.]+$")
then $c.setEmail(null); //invalidate email
end
前一条规则有一个简化的正则表达式来验证电子邮件。任何Java正则表达式都可以用于matches和not matches操作符,它们可以同时处理变量和字面量。这意味着正则表达式可以是在其他地方定义的全局变量。
集合操作是准备与一个或多个集合一起工作的,无论它们是变量、属性还是字面量。它们用于确定一个集合是否包含其中的元素。使用这些操作的一个示例类似于以下内容:
rule "print orders with pencils in them"
when
$i: Item(name == "pencil")
$ol: OrderLine(item == $i)
$o: Order($ol memberOf orderLines,
orderLines contains $ol)
then
System.out.println("order with pencils: " + $o);
end
在前面的规则中,我们使用两个操作一个接着一个。它们都在检查相同的条件,是否发现的orderLine位于order对象的orderLines集合中。
Drools的操作有一个开放的语法。这意味着,如果需要的话,您可以编写自己的操作。这是一个有用的工具,当对两个对象进行比较时,可以很容易地将其编写为一个简单的布尔操作。定制操作的一些常见用途是比较GPS定位数据,以确定在空间中彼此靠近的事实,或者比较时间数据,确定事件之间的时间关系。我们将在下一章看到更多关于自定义操作的内容。
我们已经看到规则条件是如何写的。到目前为止,所有的条件都过滤了我们传递给KieSession的数据。然而,在某些情况下,我们可能需要检查与工作内存不同的集合的特殊条件,例如某些我们可以动态创建的对象的属性、全局变量或工作内存本身的子集。为了能够做到这一功能,Drools提供了from子句,我们可以在工作内存之外定义一个特定的搜索空间。
下面的规则是我们如何使用from子句查找特定属性的一个简单示例:
rule "For every notebook order apply points coupon"
when
$o: Order($c: customer, $lines: orderLines)
OrderLine($item: item) from $lines
Item(name == "notebook") from $item
then
insert(new Coupon($c, $o, CouponType.POINTS));
end
前面的规则首先在工作内存中查找订单。它将 l i n e s 变 量 来 表 示 o r d e r L i n e s 这 个 集 合 , 以 lines变量来表示orderLines这个集合,以 lines变量来表示orderLines这个集合,以c变量来表示消费者(最后一个变量存储在规则的结果中)。找到一个订单之后,它会在$lines变量中查找,并将该项存储在另一个变量中。在此之后,规则条件的第三行直接搜索单个对象,并检查该条目是否是一个笔记本。
如你所见,如果我们的工作内存中只有Order对象(并不是所有的子组件),我们仍然可以以满足以下条件,以某种方式深入到它的结构中,:
from子句是一个非常通用的工具。它可以用于从多个源获取数据,而不仅仅是来自属性的数据。您可以从全局变量中调用返回特定值或列表的方法,并从from子句中使用这些方法。你可以在任何有返回值的东西上使用它。
PS:Drools6.3以及更高版本,有一个叫做OOPath表达式的特性,它允许以更紧凑的方式定义上一条规则,例如:
rule "For every notebook order apply points coupon"
when
$o: Order($c: customer,
/orderLines/item{name == "notebook"}
)
then
insert(new Coupon($c, $o, CouponType.POINTS));
end
全局变量的一个常见用法是检索不需要一直在工作内存中的数据。由于from子句,我们可以在执行全局变量方法时直接从规则中过滤这些数据,如下所列:
global QueryService qServ
rule "get stored coupons for new order"
when
Order(totalItems > 10, $c:customer)
Coupon(customer == $c) from qServ.queryCoupons()
then
System.out.println("We have a stored coupon");
end
这对于比较工作内存和外部存储之间的数据非常有用,比如数据库和web服务。但是,当使用这些外部查询时,我们需要注意,当引擎将调用全局变量方法时,我们需要小心。在前面的例子中,每当我们向工作内存中添加一个新的Order对象时,第一行将再次被评估。如果第一行填满了所需的条件,它将调用第二个行,调用全局变量方法。如果我们添加50个满足第一个条件的订单,那么这个规则将调用全局变量方法50次。
当使用这些条款时,要注意的另一件事是你有多深;如果您必须在相同的规则中执行五个全局变量方法,并且它们都是过程密集型的,那么您将会有一个非常慢的规则。如果您需要做这种类型的工作,最好在可能的时候使用高速缓存的缓存。
注意,如果全局变量方法的返回值发生变化,它将不会重新触发调用它的规则,即使它是规则的第一个条件。这是因为全局变量在工作内存之外,因此,当它们发生变化时,它们不会被重新评估。如果您需要重新评估from子句中使用的内容,最好的方法是使用工作内存的子集,您可以使用收集或累积关键字和from子句来创建。以下两个部分描述了这些关键字及其用法。
到目前为止,我们已经看到了在我们的规则中编写条件的方法,这些条件将会触发每一次与工作内存的匹配。让我们分析一个简单的规则来完全理解这个机制:
rule "Simple order condition"
when $o: Order()
then System.out.println("order found: " + $o);
end
前面的规则是这样定义的,如果我们在kiesession中插入50个订单,我们将在触发规则时触发50次规则。当我们希望在每个匹配条件的元素上做一些事情时,这是很有用的。然而,如果我们想要对所有符合条件的元素进行操作,我们需要额外的语法来帮助我们。这就是collect关键字发挥作用的地方。
“collect”关键字用于查找与特定条件匹配的所有元素并将它们分组到集合中。稍后,这个集合可以被分配给一个变量,或者提交给进一步的条件,如下:
rule "Grouping orders"
when $list: List() from collect(Order())
then
System.out.println("we've found " +
$list.size() + " orders");
end
在前一种情况下,如果我们将50个订单插入到订单中,然后触发规则,规则只会触发一次,生成50个订单的列表。但是,如果我们在不插入任何订单的情况下触发所有规则,这个规则仍然会在$list变量中返回一个空的列表。为了避免这种情况,我们应该在列表对象中添加条件,如下所写:
$list: List(size > 0) from collect(Order())
collect关键字可以和from关键字结合使用,来从不同的资源来创建新的集合。当我们可以通过规则条件找到对象并将它们直接添加到集合中时,这是非常有用的。
在上一节中,我们看到了collect关键字允许我们直接从一个数据源,收集信息到一个集合。这是一个非常有用的组件,当我们已经有了这些元素时,我们需要它们在工作内存中访问。然而,有时我们需要对匹配条件的数据应用转换。对于这些情况,Accumulate关键字用于将条件的每个匹配转换为特定类型的数据。
你可以把 accumulate关键字看作是更有力的collect。您不仅可以对每个匹配条件的项进行分组,而且还可以以特定的编程方式来推断这些元素的数据。
积累关键字的一些常见示例是计算匹配特定条件的元素,获取特定类型对象属性的平均值,在条件中查找特定属性的平均值、最小值或最大值。
也许一个例子可以帮助即使accumulate的用途。在前面的规则中,我们使用了一个Order对象的的名为getTotalItems的方法来返回我们订购的物品数。到目前为止,这是我们从Order对象获取信息的唯一方法。但是,使用accumulate,我们可以在使用规则的全部力量过滤特定条目的同时获得这些信息。让我们来看看这个例子:
rule "10+ items with sale price over 20 get discount"
when
$o: Order($lines: orderLines)
Number(intValue >= 10) from accumulate(
OrderLine(
item.salePrice > 20.00,
$q: quantity
) from $lines,
init(int count = 0;),
action(count += $q),
reverse(count -= $q),
result(count)
)
then
$o.increaseDiscount(0.05);
end
我们从之前的规则中得到了很多解释.它的主要目的是获取满足条件的订单中的items的总数:条件是销售价格高于20。getTotalItems这个方法可以给我们items的总数,但是,他无法通过他们进行过滤。然而,这里的accumulate将允许我们对元素应用一个条件,并应用一个谓词来转换与我们需要的信息相匹配的元素。让我们分析一下accumulate的部分,以充分理解其结构,如下:
下面的四点是accumulate(int,action,reverse和result)包含了当我们找到与条件相匹配的对象时该做什么的信息:
尽管这是使用accumulate的完整语法,但是它不一定每次都是这个复杂的。Drools提供一组预定义的accumulate函数,您可以直接在您的规则中使用它们。下面是重写的规则,以展示如何使用其中一个内置函数:
rule "10+ items with sale price over 20 get discount"
when
$o: Order($lines: orderLines)
Number(intValue >= 10) from accumulate(
OrderLine(
item.salePrice > 20.00,
$q: quantity
) from $lines,
sum($q)
)
then
$o.increaseDiscount(0.05);
end
已提供的accumulate功能如下:
首先使用预定义的函数,因为它使规则更可读,并且不容易出错。您甚至可以一次使用其中的许多,一个接一个,用逗号分隔,如下面的例子所示:
rule "multi-function accumulate example"
when accumulate(Order($total: total),
$maximum: max($total),
$minimum: min($total),
$average: avg($total)
)
then //...
end
前面的规则存储工作内存中所有订单的最大、最小值和平均值。如你所见,对于accumulate关键字,from子句不是强制性的。它甚至不鼓励多函数accumulate,因为它可能返回完全不同类型的对象(比如Lists和Numbers)。
如果你想以一种不被这些函数提供的方式的accumulate,Drools提供了一个API来创建我们自己的accumulate函数,类似于创建我们自己的自定义操作符。
在下一章中,我们将看到更多关于API的内容。
前面的部分是关于如何提取数据的。每个条件都被一个逗号分开,规则的执行基本上是每个条件后面都有一个隐式的AND。现在是时候看看如何将更复杂的布尔操作应用到我们的规则中了。
我们已经了解了如何在单个对象中使用布尔操作,检查其内部属性是否与条件的特定组合匹配。现在我们将看到如何对不同的对象进行相同的操作,以及如何将其转换为规则语言。
到目前为止,只要我们写了一个条件,这个条件就会被翻译为对于任何满足于我们条件的存在于内存中的对象的搜索。如果我们先于一个对象(或者是一个组)的条件之前使用了NOT关键字,我们可以检查是否在工作内存中找到了该条件的发生率,并为这些情况触发一个规则。让我们看一下这个关键字的例子:
rule "warn about empty working memory"
when
not(Order() or Item())
then
System.out.println("we don't have elements");
end
上面的规则使用了not关键字来确保触发规则的瞬间,我们至少有一个Order或者Item对象。如果不这样的话,它将会打印警告。
注意:我们如何使用OR关键字来在not中共享一个条件。这是因为NOT关键字不会包含一个布尔表达式。在这种情况下,因为我们想要搜索在工作内存中存在的Order或Item,我们可以将他们分组在NOT内的一个简单的表达式中。另一种书写规则的方法如下:
rule "warn about empty working memory"
when
not(Order())
not(Item())
then
System.out.println("we don't have elements");
end
它也会以同样的方式行动。
与not关键字类似,我们还有一些其他关键字可以用来检查工作内存中的元素是否存在。exists关键字用于检查满足其中的条件的对象是否存在于工作内存中。
让我们看一下以下两个规则,一个是exists,另一个没有exists
rule "with exists"
when
exists(Order())
then
System.out.prinltn(
"We have orders");
end
rule "without exists"
when
$o: Order()
then
System.out.println($o);
end
它们之间的主要区别是,这些规则将为一个或多个Order对象触发多少次。对于第二个的情况,如果你有多个订单,规则将会对每一个订单进行一次。在第一个里,不管你有一个元素,5个,还是500万个订单,你都要一次性完成这个规则。
在声明中可以看到的另一个不同之处在于第二个的规则有一个变量声明。这只能在第二个规则组件的情况下完成,因为它会为工作内存中的每一个Order都执行,你可以对每一个Order都有一个引用。在第一个规则组件的情况下,它只检查一个布尔表达式(无论订单对象是否存在于工作内存中),所以它没有存储任何我们可以在规则的结果中使用的引用。
在forall关键字中也使用了类似的模式。当我们使用后forall关键字的时候,我们检查了两种工作内存的情况。任何与第一个条件匹配的对象都必须匹配第二个条件,以使所有的都为真。
让我们来看看下面的例子:forall和exists一起工作:
rule "make sure all orders have at least one line"
when
exists(Order())
forall(
Order()
Order(orderLines.size() > 0)
)
then
System.out.println("all orders have lines");
end
在前面的规则中,您可以看到第一个条件使用exists关键字,以检查工作内存中是否有Order对象。如果有一个订单或1000个订单,这将评估一次。
规则的第二部分是使用两个条件。对于填充第一个条件的每个条目(作为Order对象),它还需要填充第二个条件(至少有一个订单行)。这意味着工作内存中的每个订单必须至少有一条命令行来触发该规则。
在相同的规则中使用exists和forall是有原因的,而不仅仅只是提供了两者的一个简短的例子。forall结构检查填充这两个条件的对象的集合是否相同。这意味着如果没有填满forall的第一个和第二个条件,它将评估为真。为了避免这个,在每一个forall之前,我们通常用exists与我们在forall关键字内的第一个条件一起使用,参考我们上面的例子。
Drools提供了各种特殊的句法功能。正如我们所希望的那样,所有这些都不适合这本书,更多的特性不断被添加。然而,当我们不得不以一种简单而全面的方式来定义我们的规则时,其中一些人就变得非常方便了。我们将讨论这些额外特性的前三种.
嵌套访问器允许我们在必须定义嵌套bean的条件时简化我们的条件。使用圆括号,它允许我们访问嵌套的属性,而不必重新声明路径以获得它们。
OrderLine( item.cost < 30.0, item.salePrice < 25.0 )
在上面这种情况下,我们过滤的订单线有成本价格低于30的商品,售价低于25美元。我们可以使用嵌套访问器来简化表达式,如下:
OrderLine( item.( cost < 30.0,salePrice < 25.0) )
这使我们能够访问item属性的多个属性,而不必重写路径来达到这些属性。
内联类型转换允许我们快速地在类型中过滤属性,而不需要从子句中滥用。它允许我们将类型的属性转换为特定的子类,并添加一个只有在转换后才有意义的条件。看例子:
Order(customer#SpecialCustomer.specialDiscount > 0.0)
在这里,我们使用#符号来标记我们的inline cast。我们有一个与我们的订单有关的客户。在某些情况下,客户不会是客户类型,而是一个名为SpecialCustomer的子类。这种类型的专业客户有一个特殊的折扣属性。在前一种情况下,我们尝试过滤具有特殊客户类型的对象的订单,在这些情况下,特殊客户的折扣大于零。使用内联转换,我们可以再一行内检查所有的这些条件。
Null-safe操作符在处理可能不完整的模型时非常有用,这样我们可能就不需要一次又一次地写空检查条件。使用一个特殊的字符,我们可以确保我们在属性上写的条件只有在属性不是空的情况下才会检查。这里有一个例子,我们使用的是null-safe操作符来访问一个订单客户的类别:
Order(customer!.category != Category.NA)
之前的情况,如果没有null-safe操作符,就会与下面的情况类似:
Order(
customer != null,
customer.category != Category.NA
)
对于深度嵌套的属性,空安全操作符允许我们保存大量的文字。
我们已经看到了一些例子,当我们发现某个特定的条件正在发生时,我们会修改工作内存。我们可以删除对象,修改现有的对象,甚至删除它们,以触发其他规则执行。我们还看到,有时候,这些元素在规则执行中是有意义的,并且在这些情况下使用声明类型。
我们是否使用声明类型的外部类,大多数情况下都暗示了以下两种策略中的一种:
在装饰现有的模型时,这些策略有一些缺点。第一个案例(添加新对象)可能意味着必须在域模型对象和以某种形式的新推断对象之间保持引用。第二种情况(修改属性)可能意味着在我们不希望这样做时修改域模型。
这些策略还有第三种选择,这意味着在不需要在原始bean中创建额外属性的情况下,向已经存在的对象添加新的特性。在工作内存中,现有对象的动态装饰被称为特质(叫特征也行,scala中称呼为特质)。
特质就像从面向对象的角度来看的形容词。我们可以说房子是漂亮的,或者汽车是漂亮的。形容词可以应用于许多不同的类型。然而,这些类型不需要共享一个公共结构。这意味着特征就像一个标识,我们可以应用于多种类型,并具有特定的属性,这些属性适用于形容词本身,因此,也适用于应用该特质的bean。
要用更简单的方式解释这一点,可以将特征看作是我们可以动态地添加到工作内存中的某些对象的额外特征。我们可以在以后的其他规则中使用这些特性来过滤这些对象。为此,我们需要做以下两件事:
declare trait KidFriendly
kidsAppeal: String
end
一旦我们遵循这些步骤,我们就可以为那些应用了特质的对象定义规则。让我们考虑一个基于我们的eShop案例的例子。我们打算以kid-friendly的方式来启动特定元素的分类,这样我们就可以根据年龄层对他们做广告。
我们可以买一些儿童友好的东西,比如彩色纸、玩具或者特殊的衣服。我们可能还会有kid-friendly的供应商(他们提供我们很多学校相关的东西)或者kid-friendly的销售渠道(就像我们的那些基于父母的网站)。如果不满足kid-friendly的资格,这些元素没有任何共同的结构。这是一个很好的应用特质的情况。
在我们开始使用这些特性之前,我们需要了解如何将trait应用于我们的对象。
当我们有一个对象想要应用某个特征时,我们可以使用don关键字来实现。它首先接收traitable对象,其次是特质类型,以及可选的第三个布尔参数,以决定是否应该在工作内存中插入逻辑。它返回一个被转换到特征类型的对象。我们看例子:
rule "toy items are kid friendly"
no-loop
when $i: TraitableItem(name contains "toy")
then
KidFriendly kf = don($i, KidFriendly.class);
kf.setKidAppeal("can play with it");
end
前面的规则定义了我们将一个TraitableItem对象视为kid-friendly的条件.规则中没有loop属性的原因是,don没有创建一个新元素,它只是在工作内存中装饰一个已有的元素。由于这个装饰没有使对象停止去满足规则条件,所以没有循环避免重新评估。
使用don关键字后,在任何其他规则中,我们都可以将这个对象看作是kid-friendly的对象。这意味着,通过KidFriendly类型对对象进行筛选的规则可以将该对象视为一个 KidFriendly的元素。
如果在某种程度上,在将一个特征应用到一个对象之后,我们会决定这个特征需要被移除,我们可以用shed的关键字来做。shed将导致与给定参数类型对应的特征的删除,如下:
Object o = shed( $traitedObject, KidFriendly.class)
这种语法,以及您可以使用的特性,可以去代码库的TraitTest中找到。
正如我们前面讨论过的,我们应该努力保持规则的简单性。要做到这一点,有时,我们将规则分解为多个规则,在引擎中插入新数据以触发其他简单的规则。这有助于保持规则的可管理性,因为更简单的规则易于理解。下面你是一个例子:
rule "determine large orders"
when $o: Order(total > 150)
then insert(new IsLargeOrder($o));
end
这样,我们就不需要多次定义我们所考虑的大订单了。如果我们想要在未来改变这个考虑,比如说,总人数大于200,我们只需要改变一次。
我们使用这种方式需要有的一个考虑就是:如果触发了对IsLargeOrder的插入的条件在未来可能不再成立。如果某个规则或某一段代码改变了订单,使他有一个更小的总数,那么IsLargeOrder对象仍将处于工作内存中。当条件不再为真时,我们可以通过创建一个规则来清除工作内存,但是规则可以帮助避免使用logical insertion(即逻辑插入)的不必要的规则重复。
对象的逻辑插入将插入的对象与触发插入的条件绑定在一起。这意味着,如果我们重写之前的规则如下:
rule "determine large orders"
when $o: Order(total > 150)
then insertLogical(new IsLargeOrder($o));
end
然后,如果在将来某一时刻,订单将其总数更改为少于150,那么将自动从工作内存中删除IsLargeOrder对象。
逻辑插入不仅避免了需要额外的规则来净化我们的工作内存,而且打开了将对象锁定到特定的条件的可能。这是很有用的,因为如果我们绑定某种形式的negation对象到一个条件上,我们可以定义我们推论的偏差或异常。
绑定一个对象的逻辑非是很简单的。只需要是用insertLogical关键字以及第二个参数是‘neg’字符串即可。下面是一个例子,我们将会为我们认为是大订单的添加一个异常,总项目小于5,不管价格是多少:
rule "large orders exception"
when $o: Order(total > 150, totalItems < 5)
then insertLogical(new IsLargeOrder($o), "neg");
end
如果我们使用前两个规则,我们将在工作内存中有一个大订单对象每个订单的总数量超过150个,项目数不超过5个。如果,某一时刻,一个订单的总数减少到150以下,相应的IsLargeOrder对象将自动被删除。如果一个订单总数超过150,并且只有4个项目得到另一个项目,那么将会自动插入一个对应的IsLargeOrder对象。
这种偏差管理具有使规则相互独立的优点。偏差规则不需要去理解有多少规则添加进来,而只需要知道对象不需要被添加进来的情况即可(The deviation rules don’t need to understand how many rules are adding, but only the situation where the object should not be added.)
请注意,逻辑插入也可以用于创建trait。don关键字有第三个可选的布尔参数如果你把它设为true,这个特质就会被逻辑地插入到工作内存中,并且只存在于当规则被评估为真时,就像下面这样:
don($traitObj, SomeTrait.class, true);
前面的方法允许我们有独立的规则,但是它不允许我们添加超过一层的偏差。如果在某一时刻,我们想要对偏差进行嵌套,那么前面的语法是不够的。让我们先来讨论一下这种双偏差情况的一个例子:
针对这类状况,Drools提供了一些列的注解,允许我们在规则内去实现偏差树。然而,这种编写规则的方法却有一个缺点。但是,使用注解会打破规则的独立性,我们必须明确规定我们提供的是偏离的规则否则你可能会发现自己有规则和偏差来相互竞争,可能导致规则的执行比设计的要多。
尽管如此,我们仍然需要做一个涉及偏差的案例,这个策略很好地管理了这个情况。提供的注释的集合标记了规则,以确定哪些是偏差,哪些是可以的,哪些是不可以的。这些注释如下:
每个规则都应该使用 insertLogical将它们的推论绑定到规则引擎。让我们来看看下面关于Drools中实现的双偏差案例的如下示例:
rule "large orders" @Defeasible
when Order($id: orderId, total>150.00)
then insertLogical(new IsLargeOrder($id));
end
rule "large orders exception" @Defeats("large orders")
when Order($id:orderId, total>150.00, totalItems < 5)
then insertLogical(new IsLargeOrder($id), "neg" );
end
rule "large orders double exception"
@Defeats("large orders exception")
when Order($id:orderId, total>300.00)
then insertLogical(new IsLargeOrder($id));
end
在之前的一套规则中,我们首先检查超过150美元的订单,然后将我们发现的所有订单都标记为大订单。第二个规则建立一个异常,表示少于5个项目的订单不是大订单。第三条规则是第二种情况的例外情况,它规定只要总数超过300美元,就可以考虑少于5项的订单。
还需要在kmodule.xml中指定。在这种情况下,KieSession将会使用可废弃的逻辑。为此,定义您的kbase和ksession标记如下:
<kbase name="ruleExceptionsKbase"
equalsBehavior="equality"
packages="chapter04.ruleExceptions">
<ksession name="ruleExceptionsKsession"
beliefSystem="defeasible"/>
</kbase>
你可以运行RuleExceptionsTest这个样例代码
规则创建的最后一个重要方面是有规则层次结构的可能性。就像类一样,规则也允许彼此之间继承。如果规则B继承了规则A,那么规则B会在其条件处,也会拥有规则A的所有条件。下表显示了使用继承的两个规则和它们的不适用继承的等价方式:
rule "A"
when
s: String(this == "A")
then
System.out.println(s);
end
rule "B" extends "A"
when
i: Integer(intValue > 2)
then System.out.println(i);
end
rule "A"
when
s: String(this == "A")
then
System.out.println(s);
end
rule "B"
when
s: String(this == "A")
i: Integer(intValue > 2)
then
System.out.println(i);
end
这是一个很好的策略,可以管理有重复条件但仍然改变结构的规则。但是,在决定使用规则继承时,您需要小心。从另一个规则继承,意味着您的子分类规则将不独立;阅读您的规则的人需要引用父规则来完全理解您的规则的行为。请谨慎使用此功能。
规则继承允许我们通过扩展现有规则来避免将条件重写为单独的规则。另一个让我们避免重写条件的有趣特性是使用有条件的命名结果的可能性。相同的标识符必须在规则条件下使用go关键字来确定什么时候应该去那个特定的结果。举例子,如果我们想把规则继承子部分中看到的两条规则写为一条规则,我们可以用以下方法来做:
rule "A and B combined"
when
s: String(this == "A")
do[aCase]
then
System.out.println(i);
then[aCase]
System.out.println(s);
end
正如上面规则你所看到的,我们,我们可以使用go关键字来标记我们现在想前往到那个条件下的结果中。如果所有的规则中的条件都是true,那么结果操作都会被执行,类似于我们定义了两个不同的规则的情况。
与使用规则的继承一样,我们需要小心的使用和这个特性。它可以为我们节省大量的重写,但它也可以触发一个规则,从长远来看,变得非常麻烦。当需要去快速更新一个已经存在的规则,这是一个很好得解决方案,但是这不是让你滥用的东西,因为继承会导致你的代码很难阅读。简单性是使规则易于理解和修改的关键。