一个复杂关系表达式过滤器的实现
背景
最近,有一个新需求,需要后台设置一个复杂的关系表达式,根据用户指定ID,解析该用用户是否满足该条件,后台设置类似于禅道的搜索条件
但是不同的是禅道有且仅有两个组,每个组最多三个条件
而我们这边组与关系可能是更复杂的,组中有组,每个条件都是有且或关系的。由于保密原因,原型就不发出来了。
看到这个需求,作为一个后端,第一时间想到的是类似QLEpress这类的表达式框架,只要构建一个表达式,通过解析表达式即可快速对目标用户进行筛选,但是可惜的是前端同学不干了,因为作为使用vue或react这类数据驱动的框架来说,将表达式转换为为上述的一个表单太难了 ,所以想了一下,决定自己定义一个数据结构,实现表达式解析。方便前端同学的处理。
分析准备
虽然是用类实现表达式,但是其本质上依旧还是个表达式,我们列举一个简单的表达式:设条件为a,b,c,d,我们随意构造一个表达式:
boolean result=a>100 && b=10 || (c != 3 && d < 50)
我们对表达式进行分析,可以发现表达式 都是共同属性有:
过滤字段(a、b、c、d),判断条件(大于、小于、不等于等等),对比值(a>100 中的100)。
另外,还有关联关系(且、或)和计算优先级这几个属性组成。
于是我们对表达式进行简化:
令a>100 =>A,b=10 =>B,c!=3=>C ,d<50=>D,于是我们得到:
result=A && B || (C && D)
现在问题来了,如何处理优先级呢?
如上表达式,很明显,这是一个大学里学过的标准的中序表达式,于是,我们画一下它的树形图:
根据这个图,我们可以明显的看到,A且B 和C且D是同一级别,于是,我们按照这个理论设计一个层级的概念Deep,我们标注一下,然后再对节点的类型做一下区分,可得:
我们可以看到作为叶子节点(上图绿色部分),相对于其计算计算关系,遇到了一定是优先计算的,所以对于深度的优先级,我们仅需要考虑非叶子节点即可,即上图中的蓝色节点部分,于是我们得到了,计算优先级这个概念我们可以转换为表达式的深度。
我们再看上面这个图,Deep1 的关系是Deep2中 A且B 和 C且D两个表达式计算出的结果再进行与或关系的,我们设A 且B 为 G1, C且D为 G2,于是我们发现关系节点关联的类型有两种类型,一种是条件Condition ,一种是组Group
至此,这个类的雏形基本就确定了。这个类包含 关联关系(Relation)、判断字段(Field)、运算符(Operator)、运算值(Values)、类型(Type)、深度(Deep)
但是,有个问题,上面的分析中,我们在将表达式转换成树,现在我们试着将其还原,于是我们一眼可以得到其中一种表达式:
result=(A && B)||(C && D)
很显然,和我们的原来的表达式并不一致,这是因为我们上述仅能记录表达式的计算顺序,而不能完全准确的表示这个表达式,这是因为在我们解析表达式的过程中,不仅是有深度、还有一个时序关系,即从左到右的顺序表示,而此时G1中的内容实际上在原表达式中的深度应该是1而不是2,然后我们引入序号的概念,将原来树变成有向的图即:
根据这个图,我们就还原出有且唯一的一个表达式了:result= A && B ||(C && D)
。
好了,我们分析了半天,原理说完了,回到最初始的问题:前后端怎么实现?对着上图想象一下,貌似还是无法处理,因为这个结构还是太复杂了。对于前端,数据最好是方便遍历的,对于后端,数据最好是方便处理的,于是这时候我们需要将上面这个图转换成一个数组。
实现方式:
上面说到了需要一个数组的结构,我们具体分析一下这个部分
我们发现作为叶子节点,可以始终优先计算,所以我们可以将其压缩,并将关系放置在其中一个表达式中形成 ^A -> &&B
或 A&& -> B$
的形式,这里我用正则的开始(^) 和结束($) 表示了一下开始 和 结束 的概念,这里为了与产品原型保持一致我们用第一种方式,即关系符号表示与前一个元素的关系,于是我们再分析一下:
再对序号进行改造:
于是我们得到最终的数据结构:
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class ExpressDto {
/**
* 序号
*/
private Integer seq;
/**
* 深度(运算优先级)
*/
private Integer deep;
/**
* 关系运算符
*/
private String relation;
/**
* 类型
*/
private String type;
/**
* 运算条件
*/
private String