一、规则引擎介绍
1、定义
复杂业务开发时,常常有复杂的判断逻辑,长期版本开发迭代后,程序本身逻辑代码和业务代码互相嵌套、错综复杂
,同时维护成本高,可拓展性差
。
规则引擎即是:可降低复杂业务逻辑组件复杂性、降低应用程序的维护和可扩展性成本的组件!
规则引擎实际上就是一个推理引擎,用于匹配facts(事实,我们可以理解为输入数据)和rules(规则),以推出结论。
2、为什么要使用?
业务规则经常变化,系统需依据业务的变化,实现快速、低成本的迭代更新。
为了快速、低成本的更新,我们需将逻辑代码和业务代码进行解耦,同时可以结合也谢配置界面进行快速的低代码能力配置:
- 维护成本低: 研发人员(不需懂业务)开发维护程序部分,同时测试通过后,后续不会经常变化改动:
- 拓展性高:业务人员可直接管理这些业务规则,同时不需要研发人员的参与。
3、规则引擎调研
参考外部文章:
https://juejin.cn/post/6972707259856093221
二、Node规则引擎 json-rule-engine
1、Engine
存储并执行规则、发出事件和维护状态
1.1)方法:
构造、添加/删除 Fact、添加/删除/更新规则、添加/移除操作符、执行运行、停止执行;
2、Facts
通常是被注册在引擎中的方法或常量,在引擎运行时会被规则条件引用到。每个Fact方法应该是一个纯函数,它可以返回计算值,也可以返回解析为计算值的Promise。
当规则在运行时计算时,会将fact值动态取回并且使用规则操作符跟规则值进行比较计算。
2.1)方法
构造;
3、Rules
规则包含一系列规则条件和一个规则事件。规则执行时,所有的规则条件都会计算。如果结果是符合的,规则事件将被触发。
3.1)方法:
构造、设置/获取规则、设置/获取规则事件、设置/获取优先级定义、转换为json格式
3.2)规则条件:
规则条件是因数(fact)、操作符、规则比较值的集合,这些信息决定了运行后是规则结果是成功还是失败。
3.2.1 规则辅助:params
有些时候因数需要额外的一些输入来进行计算。出于这个目的,params属性会作为一个参数传递到因数handler。Params 本质上起到因子参数的作用,使因子handler更加通用和可重用。
3.2.2 规则辅助:path
主要为了解决复杂类类型的属性规则值读取问题,可以通过 json-path 语法来给定一个获取fact类属性值的方式。
3.2.3 规则辅助:自定义 path 解析器
为了使用自定义的路径解析器而不是json-path提供的解析器, pathResolver回调函数选项可以传递给engine。在执行期间,当遇到 path 属性路径的时候,这个pathResolver回调函数会被调用。
相比于默认的path解析器优势:如果简单的对象遍历 DSL 提供的性能比 json-path 提供的高级表达式更好,那么这个特性可能会很有用。它还可以用于利用比 json-path 提供更高级功能的更复杂的 DSL (例如 jsonata)。
3.2.4 比较因子
有时需要比较一个因子和另一个因子。这个时候可以通过在value属性里面嵌入第二个value属性来实现。第二个因子可以访问相同的param和path属性信息。
3.3)事件:
监听规则执行之后的 success 和 failure 事件。
3.4)操作符:
常见系统自带的比较操作符
3.4.1 String和Numeric操作符:
equal、notEqual (使用严格相等比较: 相等 ===
、不相等!==
)
3.4.2 Numeric操作符:
lessThan(小于)、lessThanInclusive(小于等于)、greaterThan(大于)、greaterThanInclusive(大于等于)
3.4.3 Array 操作符:
in(fact包含在value数组中)、notIn(fact不包含在value数组中)、contains(fact 数组包含value)、doesNotContain(fact数组不包含value)
3.5)规则结果:
规则执行以后,规则结果对象会被提供给success
和failure
事件。此参数类似于常规规则,并包含关于如何计算规则的其他元数据。
规则结果可用于提取单个条件的结果、计算的fact值和布尔逻辑结果。 name
属性可以用来方便地区分给定的规则。
3.6)持久化:
规则可以很容易地转换为 JSON 并保存到数据库、文件系统或其他地方。要将规则转换为 JSON,只需调用rule.toJSON ()
方法。
之后,可以通过将 json 提供给 Rule 构造函数来还原规则。
备注: fact方法无法持久化存储原因:是一个设计的feature,具体原因:
1、根据定义,事实是为您的应用程序定制的业务逻辑,因此不在此库的范围之内;
2、很多时候,这个请求表明了一种设计气味; 试着想想其他的方法来组成规则和事实来完成同样的目标;
3、持久化事实方法将涉及到序列化 javascript 代码,并在之后通过 eval ()
恢复它。
4、Almanac
在引擎运行周期内,一个Almanac搜集了fact的信息。当引擎计算fact值时,结果存储在almanac中并缓存。如果引擎检测到一个fact的计算已经被前置计算过,它就会直接使用almanac中缓存过的值信息。每次engine.run()
被调用,一个新的almanac
就被初始化。
当前引擎的almanac可以在fact计算方法和引擎的success方法中获取到。almanac可以被用于在运行期间定义额外的fact。
4.1)方法
almanac.factValue(Fact fact, Object params, String path) -> Promise
计算提供的fact + params信息。如果path被提供了,它会在json-path中被使用;
almanac.addRuntimeFact(String factId, Mixed value)
设置一系列运行中途的常量fact。经常在引擎事件触发时使用;
almanac.getEvents(String outcome) -> Events[]
获取当前引擎的输出事件;
almanac.getResults() -> RuleResults[]
获取当前引擎执行的规则结果数据信息;
三、常用的用例demo
json-rules-engine框架本身提供了一下demo项,参见: https://github.com/CacheControl/json-rules-engine/tree/master/examples
补充一些官方demo没有的内容:
场景用例:
用例一: 判断数组对象中是否 包含/不包含 字段为某个值
const engine = new Engine();
const sceneRule = new Rule(
{
name: 'scene_rule_does_not_contain',
conditions: {
all: [
{
fact: 'store',
path: '$.book[*].category',
operator: 'doesNotContain',
value: 'reference',
}
]
},
event: {
type: 'scene_rule_does_not_contain',
params: {
data: '这里是一些payload数据'
}
}
}
);
const sceneRuleContain = new Rule(
{
name: 'scene_rule_does_contains',
conditions: {
all: [
{
fact: 'store',
path: '$.book[*].category',
operator: 'contains',
value: 'reference',
}
]
},
event: {
type: 'scene_rule_does_contains',
params: {
data: '这里是一些payload数据'
}
}
}
);
engine.addRule(sceneRule);
engine.addRule(sceneRuleContain);
const sceneFact = {
"store": {
"book": [
{
"category": "science",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
},
{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
},
{
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
}
};
const {events: eventsContain, almanac: almanacContain} = await engine.run(sceneFact);
console.log(eventsContain);
const sceneFactContain = {
"store": {
"book": [
{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
},
{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
},
{
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
}
};
const {events: eventNotContain, almanac: almanacNotContain} = await engine.run(sceneFactContain);
console.log(eventNotContain);
用例二: 判断数组对象中是否字段 全部是/全部不是 某个值
const engine = new Engine();
const sceneRule = new Rule(
{
name: 'scene_rule_all_is',
conditions: {
all: [
{
fact: 'store',
path: '$.book[?(@.category !== "reference")]',
operator: 'equal',
value: undefined
}
]
},
event: {
type: 'scene_rule_all_is',
params: {
data: 'scene_rule_all_is'
}
}
}
);
const sceneRuleContain = new Rule(
{
name: 'scene_rule_all_not',
conditions: {
all: [
{
fact: 'store',
path: '$.book[?(@.category !== "reference")]',
operator: 'notEqual',
value: undefined,
}
]
},
event: {
type: 'scene_rule_all_not',
params: {
data: 'scene_rule_all_not'
}
}
}
);
engine.addRule(sceneRule);
engine.addRule(sceneRuleContain);
const sceneFact = {
"store": {
"book": [
{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{
"category": "reference",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
},
{
"category": "reference",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
},
{
"category": "reference",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
}
};
const {events: eventsContain, almanac: almanacContain} = await engine.run(sceneFact);
console.log(eventsContain);
const sceneFactContain = {
"store": {
"book": [
{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
},
{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
},
{
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
}
};
const {events: eventNotContain, almanac: almanacNotContain} = await engine.run(sceneFactContain);
console.log(eventNotContain);
用例三: 判断数组为空或不为空
const engine = new Engine();
const sceneRuleEmpty = new Rule(
{
name: 'scene_rule_empty',
conditions: {
all: [
{
fact: 'store',
path: '$.book.length',
operator: 'equal',
value: 0
}
]
},
event: {
type: 'scene_rule_empty',
params: {
data: 'scene_rule_empty'
}
}
}
);
const sceneRuleNotEmpty = new Rule(
{
name: 'scene_rule_not_empty',
conditions: {
all: [
{
fact: 'store',
path: '$.book.length',
operator: 'notEqual',
value: 0,
}
]
},
event: {
type: 'scene_rule_not_empty',
params: {
data: 'scene_rule_not_empty'
}
}
}
);
engine.addRule(sceneRuleEmpty);
engine.addRule(sceneRuleNotEmpty);
const sceneFact = {
"store": {
"book": [],
"bicycle": {
"color": "red",
"price": 19.95
}
}
};
const {events: eventsContain, almanac: almanacContain} = await engine.run(sceneFact);
console.log(eventsContain);
const sceneFactContain = {
"store": {
"book": [
{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
}
};
const {events: eventNotContain, almanac: almanacNotContain} = await engine.run(sceneFactContain);
console.log(eventNotContain);
四、规则引擎实践
1、背景介绍:
巡检项目需求,需要执行 (1)取数据 -> (2)业务规则判断出结果 -> (3)结果告知/预警 的主业务链路逻辑。
其中: (2)业务规则判断出结果 步骤中,规则部分变更较为频繁,且不同的巡检指标处理时,各类规则可能差异较大,但是规则判断基本均为各类常见的if else语句,为了可拓展性与维护性方面考虑,引入规则引擎进行处理。项目由于使用技术为node技术,采用node规则引擎json-rule-engine来做技术实现。
2、主链路设计:
3、模块代码实现
const {Engine} = require("json-rules-engine");
const {isEmpty, has} = require("lodash");
/**
* @desc 规则引擎服务
*/
class RuleEngineService extends Service {
constructor(ctx) {
super(ctx);
this.ruleEngine = new Engine();
this.initCustomOperator();
}
/**
* 注册自定的规则比较符
*/
initCustomOperator() {
// TODO 这里添加自定义的判断操作符到规则引擎中
}
/**
* 添加规则到规则引擎
* @param ruleSetAry
*/
addRule(ruleSetAry) {
for (let rule of ruleSetAry) {
this.ruleEngine.addRule(rule);
}
}
/**
* 移除规则引擎的指定规则
* @param ruleSetAry
*/
removeRule(ruleSetAry) {
for (let rule of ruleSetAry) {
this.ruleEngine.removeRule(rule);
}
}
/**
* 执行规则
* @param ruleSetAry 规则集数组
* @param factAry 规则因子数组
*/
async exec(ruleSetAry, factAry) {
let res = [];
// 0、判空处理
if (isEmpty(ruleSetAry) || isEmpty(factAry)) {
return res;
}
// 1、引擎建立规则
this.addRule(ruleSetAry);
try {
// 2、规则执行
for (let fact of factAry) {
let factRuleResAry = [];
// 2.1 规则执行
try {
const {events, almanac} = await this.ruleEngine.run(fact);
factRuleResAry.push(events.map(({type, params}) => {
if (has(params, '_advice_parse_func') && !isEmpty(params._advice_parse_func)) {
const parser = new Function('fact', params._advice_parse_func);
params.advice.tip = parser(fact);
}
// 注意:如果满足多个条件,则这里会有多条记录信息
return {
rule_type: type,
rule_payload: params
};
}));
} finally {
// 2.2 清理执行完成的fact数据
for (let field in fact) {
this.ruleEngine.removeFact(field);
}
}
// 2.3 塞到结果的定义队列里面
res.push(
{
fact: fact,
ruleResult: factRuleResAry
}
);
}
} finally {
// 3、清理规则
this.removeRule(ruleSetAry);
}
return res;
}
}