一个简单但功能强大的规则引擎(译文)

本期关键词

  • evaluate  [ɪˈvæljueɪt]
    v. 评价;估价;求…的值

  • clarify  [ˈklærəfaɪ]
    v. 澄清;阐明

  • token  [ˈtəʊkən]
    n. 代币,辅币;表示;记号;(计算机)令牌
    adj. 装样子的;作为标志的;象征性当选的


我希望有个这样的规则引擎,它是轻量级的、简单的,但是功能强大的。虽然有些产品非常好,但相应的学习成本也很高。而且呢,我幻想着能够写一个自己的规则引擎。以下是我的一些基本要求:

  • 用某种表达语言来编写规则

  • 规则应当可以保存在数据库中

  • 规则有优先级,只有最高优先级的才能被触发

  • 也可以触发所有匹配的规则

  • 规则基于一个输入进行求值,这个输入可以是一个类似树的对象,包含规则求值需要的所有信息

  • 当某些规则被触发时,应当执行系统中已规划的预定义动作

为了便于阐明这些需求,考虑以下示例:
1)在某些论坛系统中,管理员需要配置何时发送邮件
这里,我会写这样的规则。比如,”当名为 sendUserEmail 的配置项设为 true 时,向用户发送一份邮件“;”当名为 sendAdministratorEmail 配置项设为 true,且用户发帖数少于 5 时,向管理员发送一封邮件“。

2)关税(tarif)系统需要可配置,以便为客户提供最优惠的关税
为了向客户提供最优的关税,我会写这样的规则。如,“年龄小于 26岁适用于 ‘年轻关税’ ”;“年龄大于 59岁,适用于 ‘年长关税’”;“年龄介于 26 到 59之间,适用于 默认关税,除非他的账号期限超过 24 个月,则适用于 ‘忠实会员关税’”。

3)火车票可以视为一种产品,不同的旅行需求,适配不同的产品
这里的规则可以是这样,“行程超过 100 公里且要求一等座,则出售产品 A”。

4)时间表软件需要决定学生什么时候可以离开学校
其中一条规则可能是:“如果一个班有10岁以下的学生,则整个班都可以提前离开;否则,在正常时间离开。”

带着这些要求,我开始寻找一种表达式语言。从JSP 2.1中指定的统一表达式语言开始,通过使用 Tomcat和Apache Commons EL jar中用到的jasper jar,我很快地安装并运行起一些东西。然后我从 Codehaus.org上发现了 MVEL库,它恰好也被用于Drools(领先的 Java规则引擎?)中,并且它的运行效果更好。据我所知,它提供了更多的功能特性。

因此,我将自己的规则引擎设计成这样:

1) 配置一些规则
2) 一个规则有已下一些属性:

  • 命名空间(namespace): 引擎可能包含许多规则,但是只有部分规则与特定的调用相关,可以使用命名空间进行过滤

  • 名称(name): 一个命名空间中,名称是唯一的

  • 表达式(expression): 规则的 MVEL 表达式

  • 结果(outcome): 当规则表达式计算结果为 true 时,引擎可能会使用的字符串

  • 优先级(priority):一个整数。值越大,优先级越高

  • 描述(description): 规则描述,便于进行规则管理

3) 给引擎一个输入对象,计算所有规则(或是一个名称空间内的规则),然后返回如下:
a) 返回所有计算结果为 true 的规则
b) 从所有计算结果为 true 的规则中,返回具有最高优先级的规则的结果(字符串)
c) 从所有计算结果为 true 的规则中,找出最高优先级的规则,执行与其结果相关联的操作(在应用程序中定义的)

4) “Action” 是应用程序员提供的类的实例。每个 action 都有一个名字。当引擎被要求根据规则执行某个操作(action)时,它将会执行与“获胜”规则的结果名称匹配的操作。

5) 规则可以由“子规则集”组成。子规则用作构建块,在此基础上建立更复杂的规则。在计算规则结果时,引擎永远不会选择一个子规则作为最佳(最高优先级)的规则(即计算结果为 true)。子规则使构建复杂规则变得更容易,稍后我会展示。

那么,是时候展示一些代码了。
首先,来看看 tarif 系统的代码:

Rule r1 = new Rule("YouthTarif", "input.person.age < 26", "YT2011", 3, "ch.maxant.someapp.tarifs", null);
Rule r2 = new Rule("SeniorTarif", "input.person.age > 59", "ST2011", 3, "ch.maxant.someapp.tarifs", null);
Rule r3 = new Rule("DefaultTarif", "!#YouthTarif && !#SeniorTarif", "DT2011", 3, "ch.maxant.someapp.tarifs", null);
Rule r4 = new Rule("LoyaltyTarif", "#DefaultTarif && input.account.ageInMonths > 24", "LT2011", 4, "ch.maxant.someapp.tarifs", null);
List<Rule> rules = Arrays.asList(r1, r2, r3, r4);

Engine engine = new Engine(rules, true);

TarifRequest request = new TarifRequest();
request.setPerson(new Person("p"));
request.setAccount(new Account());

request.getPerson().setAge(24);
request.getAccount().setAgeInMonths(5);
String tarif = engine.getBestOutcome(request);

上面的代码中,我添加了4条规则到引擎中,并告诉引擎当有规则无法预编译时抛出异常;然后,创建了一个 TarifRequest 对象,作为输入;然后将该对象传递给引擎,要求引擎给出最好结果。在这个例子中,最好的结果是字符串“YT2011”,这是 TarifRequest 请求的客户最适合的 tarif名称。

这一切是如何运作的?
当引擎拿到规则时,会进行一些验证,并对规则进行预编译(以提高整体性能)。注意下前两条规则是如何引用一个名为“input”的对象的, 该对象正是传递到引擎上的“getBestOutcome”方法的参数对象。引擎将输入对象连同每个规则表达式一起传递给 MVEL类。每当一个规则的表达式计算结果为“true”时,它就会成为一个候选规则,参与竞争最终获胜规则。最后,按照候选规则优先级排序,引擎将会返回最高优先级的规则的结果字段。

注意到第三和第四个规则中包含'#'字符。这不是标准的 MVEL表达式语言。当规则传递给引擎时,引擎会检查这些规则,并将以散列符号(即'#')开头的任何标签替换为与标签同名的规则中的表达式,表达式会放在括号中。在解析并替换了引用规则之后,日志记录器将输出完整的规则,以便于你检查规则。

上述业务案例中,我们只对客户的最佳 tarif感兴趣。或许我们也会关心可能的关税清单,这样可以为客户提供一种选择。这种情况下,可以调用引擎上的“getMatchingRules”方法,该方法返回按优先级排序的所有规则。tarif 名称(本例中)对应规则的 “outcome”字段。

在上面的例子中,我想从四个规则中得到其中的一个结果。然而,有时我们可能希望基于构建块组建复杂的规则,同时并不希望这些构建块成为一个成功的结果。前面火车旅行的例子可以用来说明我的意思:

Rule rule1 = new SubRule("longdistance", "input.distance > 100", "ch.maxant.produkte", null);
Rule rule2 = new SubRule("firstclass", "input.map[\"travelClass\"] == 1", "ch.maxant.produkte", null);
Rule rule3 = new Rule("productA", "#longdistance && #firstclass", "productA", 3, "ch.maxant.produkte", null);
List<Rule> rules = Arrays.asList(rule1, rule2, rule3);

Engine e = new Engine(rules, true);

TravelRequest request = new TravelRequest(150);
request.put("travelClass", 1);
List rs = e.getMatchingRules(request);

在上面的代码中,我用两个子规则构建了 rule3,但并不希望引擎输出这些构建块的结果。子规则没有结果字段和优先级,它们用来建立更复杂的规则。在初始化过程中,引擎使用子规则替换所有以散列符开头的标签之后,它会丢弃子规则(它们不会被计算)。TravelRequest 类构造函数以 distance 为参数,并且包含一个额外的 map 属性。使用规则2 中的语法可以轻松地访问 map 值。

接下来,考虑配置论坛系统的业务用例。下面的代码介绍了 ‘操作’(Action)。‘操作’由应用程序编写者创建并提供给引擎。引擎接受规则结果(如第一个示例中所述),查找与这些结果同名的‘操作’,并调用其 execute方法(实现了 IAction接口)。当系统需要执行预定义的操作,但操作的选择需要高度可配置且与部署无关时,此功能非常有用。

Rule r1 = new Rule("SendEmailToUser", "input.config.sendUserEmail == true", "SendEmailToUser", 1, "ch.maxant.someapp.config", null);
Rule r2 = new Rule("SendEmailToModerator", "input.config.sendAdministratorEmail == true and input.user.numberOfPostings < 5", "SendEmailToModerator", 2, "ch.maxant.someapp.config", null);
List<Rule> rules = Arrays.asList(r1, r2);

final List<String> log = new ArrayList<String>();

Action<ForumSetup, Void> a1 = new Action<ForumSetup, Void>("SendEmailToUser") {
  @Override
  public Void execute(ForumSetup input) {
    log.add("Sending email to user!");
    return null;
  }
};
Action<ForumSetup, Void> a2 = new Action<ForumSetup, Void>("SendEmailToModerator") {
  @Override
  public Void execute(ForumSetup input) {
    log.add("Sending email to moderator!");
    return null;
  }
};

Engine engine = new Engine(rules, true);

ForumSetup setup = new ForumSetup();
setup.getConfig().setSendUserEmail(true);
setup.getConfig().setSendAdministratorEmail(true);
setup.getUser().setNumberOfPostings(2);

engine.executeAllActions(setup, Arrays.asList(a1, a2));

上述代码,在调用 executeAllActions 方法时,‘操作’被传递给引擎。本例中,两个‘操作’都会被执行,因为 setup 对象输入使得两个规则计算结果都为 true。请注意,这些‘操作’是按照规则优先级执行的(in the order of highest priority rule first)。每个‘操作’只执行一次——它的名称在执行之后被记录下来,不会再被执行,直到再次调用引擎的 “executeaction”方法。此外,如果只想执行与最佳结果相关的操作,请调用“executeBestAction”方法,而不是“executeAllActions”方法。

最后,我们看下课堂的例子。

String expression =
    "for(student : input.students){" +
    "if(student.age < 10) return true;" +
    "}" +
    "return false;";

Rule r1 = new Rule("containsStudentUnder10", expression , "leaveEarly", 1, "ch.maxant.rules", "If a class contains a student under 10 years of age, then the class may go home early");

Rule r2 = new Rule("default", "true" , "leaveOnTime", 0, "ch.maxant.rules", "this is the default");

Classroom classroom = new Classroom();
classroom.getStudents().add(new Person(12));
classroom.getStudents().add(new Person(10));
classroom.getStudents().add(new Person(8));

Engine e = new Engine(Arrays.asList(r1, r2), true);

String outcome = e.getBestOutcome(classroom);

上面的结果是“leaveEarly”,因为教室里有一个年龄不到10岁的学生。MVEL 可以编写一些相当全面的表达式,它本身就是一种编程语言。引擎只需要一个规则返回 true,如果该规则被认为是要触发的候选规则。

在源码 JUnit 测试用例中有更多的例子。

目前来看,除了”规则应当可以保存在数据库中“,其他需求都已经实现。虽然这个库不支持向数据库读写规则,但规则是基于字符串的。因此,编写一些JDBC或 JPA代码,实现从数据库中读取规则、填充规则对象,并将它们传递给引擎并不困难。通常这些东西以及规则管理都是与具体项目相关的,所以我没有将其添加到库中。而且我的库可能永远不会像 Drools 那样酷或者流行,我不确定是否值得去添加这样的功能。

我已将规则引擎放入一个具有 LGPL许可证的 OSGi库中,可以从我的工具站点下载它。这个库依赖于 MVEL,可以在这里下载(ps:貌似已经无法访问了)(我使用的是2.0.19版本)。如果你喜欢,请告诉我!

文章来源:

http://blog.maxant.co.uk/pebble/2011/11/12/1321129560000.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值