发布于 

基于drools语法实现衍生变量功能的设计

在使用drools的过程中,我们期望有一种衍生变量功能,它能支持我们对用户现有变量做组合加工,从而生成新的变量,这个变量的使用应该和现有的变量一致,思考整个方案记录入文。

概述

在现有的设计中,我们使用页面拖拉选择用户变量并编辑条件、按照drools的语法生成规则脚本,生成的结果类似于下边这样:

1
2
3
4
5
6
7
8
9
import java.util.*;

rule "rule01"
when
$m:Map()
eval(true) && Map(this["A"]>=1000) && Map(this["B"]==5)
then
$m.put("RESULT",true);
end

如上规则中的AB都是用户宽表中变量,需要提前计算好并存入数据库中,跑规则之前通过用户证件号去查询获取,塞进Map中再跑规则。

但是,在实际业务中,还存在很多用户变量,他其实可以根据用户现有的一些变量推算出来(比如 C = MIN(A,5) * B / 5。假如我们不做修改,那么,还是需要写死代码、写sparkSQL去把每个用户的C算出来写进数据库。每次增加类似变量,都需要做一次代码开发、测试、投产流程。

因此,我们需要这么一种衍生变量功能,它能够支持我们对用户现有变量做组合加工,从而生成一个新的变量,这个变量的使用——包括拖拉选择、条件设置、脚本生成、页面查询——都应该跟现有简单变量一致,但它的来源不是其他业务代码的计算,而是我们系统实时对现有变量的组合加工而成。

思路

还是考虑上边的例子,假如我们通过页面编辑客户达标条件为a >= 1000 && B == 5 && C == 5.0,那么,我们最终期望生成的代码应该是这样:

1
2
3
4
5
6
7
8
9
import java.util.*;

rule "rule01"
when
$m:Map()
eval(true) && Map(this["A"]>=1000) && Map(this["B"]==5) && Map(this["C"] == 5.0)
then
$m.put("RESULT",true);
end

由于变量CAB加工而成,即C = MIN(A,5) * B / 5,因此,可以把上边的规则脚本修改为:

1
2
3
4
5
6
7
8
9
10
import java.util.*;
global me.ltang.DroolsFunctionNumber num;

rule "rule01"
when
$m:Map()
eval(true) && Map(this["A"]>=1000) && Map(this["B"]==5) && eval(num.MIN($m.get("A"), 5) * Integer.parseInt(String.valueOf($m.get("B"))) / 5.0 == 5.0)
then
$m.put("RESULT",true);
end

上边的脚本中,我把取最大值、最小值、取小数位等drools不方便表达的运算操作封装在DroolsFunctionNumber类中(如下示),可以直接在脚本中调用。但还有个问题就是,由于我们的map是Map<String,Object>,因此get到的Object不能直接用于数学运算(+-*/等),还需要额外的代码(Integer.parseInt(String.valueOf(...)))将Object转为数值类型。因此,我也在考虑是否将加减乘除取余等数学操作,也封装在DroolsFunctionNumber中,这样,生成的脚本会简短清晰一些。不过,不管怎样,这并不影响大局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class DroolsFunctionNumber {
/**
* 获取最小值
*
* @param ns
* @return
*/
public static double MIN(Object... ns) {
double min = Double.MAX_VALUE;
for (Object n : ns) {
if (n == null) {
continue;
}
double dn = Double.parseDouble(String.valueOf(n));
if (dn < min) {
min = dn;
}
}
return min;
}

public static Double SCALE(Object n, Integer scale, String rondingMode) {
...

因此,除了现有的规则编辑功能外,我们需要提供新的衍生变量编辑功能,让用户可以在页面编辑 C = ...,然后,在通过规则配置生成脚本时,把C对应的计算逻辑替换到脚本中C的位置。

优化

在上边的设计中,C的值是每次执行规则时由drools算出来,这样有两个缺点:

  1. 假如在一个活动规则里面,C的值被多次用到,那么就需要多次计算C的值;
  2. 假如在java代码里面需要用到这个C的值,我们还无法从用户变量(Map<String,Object>)中获取,还需要额外的drools脚本将这个值计算出来

因此,考虑这么一种优化:在真正执行业务逻辑的规则代码之前,执行变量C的计算逻辑,将算好的值put进用户变量map,后续的规则和代码中可以直接使用。优化后的规则脚本如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.*;
global me.ltang.DroolsFunctionNumber num;

rule "value_C"
salience 1
no-loop true
when
$m:Map(this["A"] != null, this["B"] != null)
then
$m.put("C", num.MIN($m.get("A"), 5) * Integer.parseInt(String.valueOf($m.get("B"))) / 5.0);
update($m)
end

rule "rule01"
when
$m:Map()
eval(true) && Map(this["A"]>=1000) && Map(this["B"]==5) && Map(this["C"] == 5.0)
then
$m.put("RESULT",true);
end

如上,我们在value_C规则中,将C的值计算出来了,后边的业务规则rule01,只需要按现有的模式——将它当成一个普通的变量——使用即可,不管业务代码里面使用了几次C,都只需要计算一次;此外,执行完这个规则脚本后,后续的代码中想使用C的值,直接map.get("C")即可。

当然,这样有一个缺点,假如业务代码中A/B其实不满足条件,那么C其实是不需要计算的,但这个方案中,不管A和B满不满足条件,我们都先把C算出来了,这也是一个成本。不过,两权相利取其重,还是使用优化方案比较好。

想要更多

在上边的设计中,我们实现了对数值类型的衍生变量的支持,但其实,有时候我们不满足于数值类型。举个简化的例子,用户M,如果他的属性A为true, 属性B为1000,那么我认为这个用户有个衍生的属性C的值为”高附加值用户”,否则为”低附加值用户”。那么,我的规则可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.*;

rule "value_C_01"
salience 1
no-loop true
when
$m:Map(this["A"] == true && this["B"] == 1000)
then
$m.put("C", "高附加值用户");
update($m)
end

rule "value_C_02"
salience 1
no-loop true
when
$m:Map(this["A"] != true || this["B"] != 1000)
then
$m.put("C", "低附加值用户");
update($m)
end

不过,这里需要注意的是,我们如何对用户(业务)提供编辑能力?理论上,对于数值类型的C,用户只需要编辑C的取值逻辑为num.MIN($m.get("A"), 5) * Integer.parseInt(String.valueOf($m.get("B"))) / 5.0即可,其他的语法,我们可以自动替用户补全。但是对于最后例子中则不适用了,除非,我们把规则脚本改写成下面这样子:

1
2
3
4
5
6
7
8
9
rule "value_C"
salience 1
no-loop true
when
$m:Map()
then
$m.put("C", String.valueOf($m.get("A")).equals("true") && String.valueOf($m.get("B")).equals("5000") ? "高附加值用户":"低附加值用户");
update($m)
end

这样,业务仅仅需要编辑C = String.valueOf($m.get("A")).equals("true") && String.valueOf($m.get("B")).equals("5000") ? "高附加值用户":"低附加值用户"。不过,这样的话,除了同样恶心的类型转换外,还需要额外加个判断——需不需要对AB做判空处理, 避免因为A/B值为null导致计算异常。

总的来说,我是倾向于最后这种方案,可以适用于大部分的衍生变量计算,至少能满足现有需求了。