一个基于表格的规则引擎的例子
早在研究生的时候,导师就让我去了解过规则引擎,这个概念本身一点都不新鲜。不过直到最近才慢慢加深了一些体会。
规则引擎(Rule Engine,更完整地,Business rules Engine)的定义在Wikipedia和百度百科上都有。可以简单理解为如果有大量的IF-ELSE/SWITCH-CASE类的逻辑,将这些逻辑做成可插拔的外部模块,这些逻辑不属于代码本身,改动也可以做到不需要上线(或者不需要更改代码)。
下面的一些实现中,能做到可插拔的其实很少,一般只能做到逻辑隔离。
1. 使用场景
先从两个场景来理解理解什么时候可以上规则引擎
- 场景1:
产品要依据条件 C 1 , C 2 , . . . , C 10 C_1, C_2, ... , C_{10} C1,C2,...,C10定义人群,每一个人群可能只用了其中两三种条件的组合。如果每一次都需要写代码去搞定这个事就太麻烦了。
简单的场景不足以说明使用规则引擎的意义,下面的场景2就更有些意义了。
- 场景2:
基于规则做在线异常访问拦截,可能:
- 已经定义的规则可能想临时(在线)调整一些条件的阈值。(好吧,我觉得这个好像在ZK上配一下就行了)
- 增加新的逻辑条件(靠自己手写,好像开始有点困难了)
- 一个访问来了,怎么看到它符合什么样的条件,命中了哪些规则?
- 接3,竟然没有命中预期的规则,怎么调试呢?
往往问题比较简单的时候(比如 1)会让人根本不屑于使用规则引擎,稍微复杂一点可能觉得还能挣扎一下(到2了)。到了3/4这个层面的时候可能就会觉得这事有点意思了。
2 Java实现的规则引擎
Baeldung上有一篇介绍各种引擎的文章List of Rules Engines in Java,不再多做介绍,只列一下这些引擎:
- Drules
- OpenL Tablets
- Easy Rules
- RuleBook
但是走完引擎的介绍可能还是会有一些疑问:
- Drules的规则语言跟java很像,但又好像有点区别,有点拒绝不想学;要是写规则就是写代码,那和直接写代码有什么区别?
- 规则确实是就是直接用Java代码写的,比如下面的RuleBook的一段规则定义示例
public class HelloWorldRule {
public RuleBook<Object> defineHelloWorldRules() {
return RuleBookBuilder
.create()
.addRule(rule -> rule.withNoSpecifiedFactType()
.then(f -> System.out.print("Hello ")))
.addRule(rule -> rule.withNoSpecifiedFactType()
.then(f -> System.out.println("World")))
.build();
}
}
这到底跟直接手写代码有什么区别?
这里就有必要引出一类基于表格的,最好是能基于Excel的。Drules支持,OpenL Tablets也支持,考虑到网上有反馈说Drules基于Excel的条件表调试起来比较困难,下面的示例都是基于OpenL Tablets的。
比如上面的一个简单表格表达的是:如果derivationCat=$WeiboTrends
,并且device=VoiceAssitant
那么就返回分数0.5;在其它情况下返回0.5 。
用表格来表达规则:
优点
- 条件能够清晰罗列,比较容易发现问题
- 实现简单,把所有条件算一遍,也不用考虑算还是不算,算完塞进去就可以了
缺点
- 表格在表达“或”逻辑上会有一些麻烦,比如这里要求derivationCat是
$WeiboTrends/$WhoClause/$WhetherClause
之一,那么就得把这一行重复三遍了。 - 条件不能太多,基本上十几个条件就已经到人的极限了。这个时候需要把问题拆分成子问题再来应用会好一点。
3 规则引擎背后的算法和规范
Rete算法: Wikipedia中的介绍比较复杂,可以简单了解个大概印象就够了——大量的条件重复计算会浪费时间,把条件拆分成细粒度存储结果、重复使用结果,这样就能减少运算量。这里就涉及到性能考量了,这个大可不必担心,跟机器学习的模型开销比起来不值一提。
Java是有定义规则引擎API JSR-94,可以参考IBM的文章了解了解 Java规则引擎与其API(JSR-94)
另外,一般来说规则引擎中的每一条规则都是IF-THEN形式的,没有ELSE。在大部分实现中,命中一条IF-THEN就不再执行其它的。理解这一点后可以稍加利用,注意规则的排列顺序,可以减少条件的书写。在理想情况下可以跟手写IF-ELSE达到相同效果。
4. 在一个打分场景下的应用示例
交待一下在使用规则引擎之前的样子吧,有几十个if-else
,条件有重复计算甚至还有互相矛盾的逻辑,条件多了之后要理顺这些就比较困难了。
if (logPop > 6) directScore
else if (item.personDomainScore < 0.3) directScore * 0.667
else {
top2PvDiff(item) match {
case Some(diff) =>
...
if (logPop > 6) score
else if (logPop > 4) score * 0.6
else score * 0.2
case None =>
if (logPop < 5) {
if (summary.length > 100) directScore
else 0.3
} else directScore
}
}
比如:第一个logPop > 6
已经判断了,显然下面的没有判断>6的必要了。
交给规则引擎做会是什么样的?
- 填充各种各样的条件,没有填null,其中
DoubleValue
和IntValue
是可以表达范围的。
SimpleRules Double scoreDirectName(
String l1Cause,
String l2Cause,
DoubleValue intentScore,
IntValue lexiconLength,
String lexiconLanguage,
Boolean lexiconAliasOfPerson,
Boolean lexiconInSynonymTable,
Boolean lexiconIsTopFamilyName,
Boolean lexiconIsTranslated,
Boolean lexiconEnglishNameEquals,
DoubleValue individualPopularity,
Boolean individualHasSynonym,
IntValue individualSummaryLength,
DoubleValue distPersonDomain,
String distDomainPersonOrder,
DoubleValue distDomainTop2Diff
)
- 调用规则引擎实例打分
val grader: SimpleRules = {
val engineFactory =
new RulesEngineFactory[SimpleRules]("conf/Grader.xlsx", classOf[SimpleRules])
engineFactory.newEngineInstance()
}
grader.scoreDirectName(...)
-
调试(可以在网页上调试!OpenL Web Studio)
输入一些条件,条件多了还可以考虑使用Json输入的方式。
看看命中了哪条规则,图中的各种颜色的意思是:白色是部分命中的条件,灰色表示条件不匹配,绿色表示命中的规则。
小结
这个例子太简单了,代码示例呢?❌️ 基于表格的规则引擎使用,确实不需要什么代码。在表格抽象层次上,终于能让规则引擎有用一回了。