【规则引擎】规规则引擎的介绍与项目实践( 基于json-rule-engine)

一、规则引擎介绍

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)规则结果:

规则执行以后,规则结果对象会被提供给successfailure事件。此参数类似于常规规则,并包含关于如何计算规则的其他元数据。
规则结果可用于提取单个条件的结果、计算的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;
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值