不知道你是否想过:公司年会抽奖的时候、中大奖的都是我们的领导?而不是我们这种小卡拉米?如果你想知道的话。不妨看看此文~
本文主要描述了如何实现一个规则引擎,要实现的功能为通过输入员工的信息【性别、年龄 或者工龄】在年会抽奖的过程中、使其抽奖得到年会奖品。如抽奖者信息为女性、22岁使其得到200w的公司年会奖金。
数据模型:由于存在多个判断条件、且相互之间有一定的依赖性,如需要先判断性别、在判断年龄、在判断职位信息,图如下:
这就是数据结构中常见的树结构,刷过leetcode 的都知道,首先定义一个TreeNode节点类、表示一个个节点:
public class TreeNode {
private Long treeNodeId;
// 1 非叶子节点 2 叶子节点
private Long nodeType;
// 当前节点的判断规则
private String ruleKey;
// 当为叶子节点时,才有值,表示决策的结果
private String nodeValue;
// 用于描述每一条线,
private List<NodeLine> nodeLines;
}
在定义一个 NodeLine 类,用于描述树中的路径:
public class NodeLine{
private Long nodeLineId;
// 树节点的id
private Long treeNodeId;
private Long fromNodeId;
private Long toNodeId;
// 0 大于 1 小于 2 等于 3 大于等于 4 小于等于
private int nodeLineType;
// 是否满足这个值,满足这两个判断条件才能够找到当前节点的下一个节点。
private String nodeLineValue;
}
基于这两个类就可以组装一棵规则树了。对于每一个非叶子节点而言,首先要找到当前节点的判断规则,其次要根据用户的当前节点值,遍历nodeLine属性找到下一个节点,一直找到包含奖品信息的叶子节点。
规则树创建出来之后,就需要根据这棵树进行逻辑判断,不同的节点的判断依据不一样,依赖节点中的 ruleKey 属性,通过 ruleKey属性获取到当前节点的判断器、通过比较 nodeLine 的值和用户信息的值,确定是否能够达到下一节点。定义一个如下的接口:
// 所有非叶子节点的 common 行为
public interface LogicFilter {
// 获取用户当前所在节点的节点值,在gender节点获取【man | woman】 在age节点获取【年龄】
String getValue(UserRequest request);
// 获取当前节点能否进入下一个节点,value:用户当前节点的节点值
Long getNextNode(String value, List<NodeLine> nodeLines);
}
使用抽象类进行简单的实现:
public abstract class BaseLogic implements LogicFilter {
// 不同的节点获取值的方式不一样,需要交给具体的filter实现
@Override
public abstract String getValue(UserRequest request);
@Override
public Long getNextNode(String value, List<NodeLine> nodeLines) {
for (NodeLine nodeLine : nodeLines) {
if (decisionLogic(value, nodeLine)) {
return nodeLine.getToNodeId();
}
}
// return null 也可~
return -1L;
}
// 判断
public boolean decisionLogic(String value, NodeLine nodeLine) {
switch (nodeLine.getNodeLineType()) {
case 1:
return value.equals(nodeLine.getNodeLineValue());
case 2:
return Integer.parseInt(value) > Integer.parseInt(nodeLine.getNodeLineValue());
case 3:
return Integer.parseInt(value) < Integer.parseInt(nodeLine.getNodeLineValue());
case 4:
return Integer.parseInt(value) >= Integer.parseInt(nodeLine.getNodeLineValue());
case 5:
return Integer.parseInt(value) <= Integer.parseInt(nodeLine.getNodeLineValue());
default:
return false;
}
}
}
以年龄和性别为例:实现方式如下
public class UserGenderFilter extends BaseLogic {
@Override
public String getValue(UserRequest request) {
return request.getInfos().get("userGender");
}
}
public class UserAgeFilter extends BaseLogic {
@Override
public String getValue(UserRequest request) {
return request.getInfos().get("userAge");
}
}
将其放到一个容器中,共我们后续使用:
public class LogicContext {
public static final Map<String, LogicFilter> LOGIC_FILTER_MAP = new HashMap<>();
static {
LOGIC_FILTER_MAP.put("userAge", new UserAgeFilter());
LOGIC_FILTER_MAP.put("userGender", new UserGenderFilter());
}
}
UserRequest是一个封装了用户信息的JavaBean,主要属性如下:
private Long userId;
private Long ruleTreeId;
private Map<String, String> infos;
有了节点和判断器,就需要将其组装起来使用。定义一个引擎接口:这是给客户端调用的接口。
public interface Engine {
FilterResponse process(UserRequest request);
}
简单实现如下:
public abstract class BaseEngine implements Engine {
@Override
public FilterResponse process(UserRequest request) {
throw new RuntimeException("not implement");
}
protected FilterResponse makeResponse(TreeInfo treeInfo, UserRequest request) {
// 根节点id
Long treeRootId = treeInfo.getRoot().getTreeRootId();
System.out.println("treeRootId:" + treeRootId);
// 获取到第一个root节点的详细信息
TreeNode treeNode = treeInfo.getTreeNodeMap().get(treeRootId);
Long nextNodeId = 0L;
// 当下一个节点不为 null 且 非叶子节点时,一只遍历直到找到叶子节点
while (treeNode != null && treeNode.getNodeType() != 2l) {
// 得到当前节点的判断规则
String ruleKey = treeNode.getRuleKey();
LogicFilter logicFilter = LogicContext.LOGIC_FILTER_MAP.get(ruleKey);
String value = logicFilter.getValue(request);
nextNodeId = logicFilter.getNextNode(value, treeNode.getNodeLines());
System.out.println("nextNodeId = " + nextNodeId);
if (nextNodeId == -1l || nextNodeId == null){
return new FilterResponse(false, "fail");
}
treeNode = treeInfo.getTreeNodeMap().get(nextNodeId);
}
// 封装结果
FilterResponse filterResponse = new FilterResponse();
if (nextNodeId > 0L && treeNode != null) {
filterResponse.setSuccess(true);
filterResponse.setMessage("success" + treeInfo.getTreeNodeMap().get(nextNodeId));
} else {
filterResponse.setSuccess(false);
filterResponse.setMessage("fail");
}
return filterResponse;
}
}
子类实现:
public class EngineHandler extends BaseEngine {
// 包含所有决策树到容器 模拟当前系统中查出来的所有决策树信息cache
public static Map<Long, TreeInfo> treeInfoMap = new java.util.HashMap<>();
@Override
public FilterResponse process(UserRequest request) {
Long ruleTreeId = request.getRuleTreeId();
// mock db cache 操作
TreeInfo treeInfo = treeInfoMap.get(ruleTreeId);
if (treeInfo == null) {
return null;
}
return makeResponse(treeInfo, request);
}
}
客户端使用,首先组装 treeInfoMap结构,这一步模拟系统中缓存了决策树信息的Cache。决策树的结构如第一张图。
构建如下的用户请求:
我是在EngineHandler中通过主方法测试的:
public static void main(String[] args) {
EngineHandler engineHandler = new EngineHandler();
UserRequest request = new UserRequest();
request.setRuleTreeId(1L);
Map<String, String> infos = new HashMap<>();
infos.put("userGender", "woman");
infos.put("userAge", "22");
request.setInfos(infos);
FilterResponse process = engineHandler.process(request);
System.out.println(process.isSuccess());
System.out.println(process.getMessage());
}
// output:
//successTreeNode{treeNodeId=7, nodeType=2, ruleKey='null', nodeValue='ge18: 200w大奖', nodeLines=null}
到这里是否明白了为什么中大奖的不是你?