美文网首页
规则引擎

规则引擎

作者: buffonme | 来源:发表于2022-04-13 12:47 被阅读0次

    什么是规则引擎

    业务场景一般都是杂糅繁复的,于是代码很容易就互相嵌套、错综复杂、结构不清晰,同时维护成本高,可读性可拓展性差

    规则引擎:整合了传入系统的Fact集合和规则集合,以推出结论。可以理解为当下一些状态集合(有限状态机)的情况下,去触发(推论出)一个或多个业务操作,可以表示为“在某些条件下,执行某些任务”

    在拥有大量规则和Fact对象的业务系统中,可能会出现多个Fact输入都会导致同样的输出,这种情况通常称作规则冲突。规则引擎可以以下冲突解决方案来确定冲突规则的执行顺序:

    正向链接:(基于“数据驱动”的形式),规则引擎利用可用的Fact推理规则来提取出更多的Fact对象,直到计算出最终目标,最终会有一个或多个规则被匹配,并执行。因此,始于事实,始于结论。

    反向链接:(基于“目标驱动”的形式),从规则引擎假设的结论开始,如果不能够直接满足这些假设,则搜索可满足假设的子目标。规则引擎会循环执行这一过程,直到证明结论或没有更多可证明的子目标为止

    规则引擎可以被视为复杂的if / then语句解释器。if部分表示处理条件;then部分表示执行的操作

    举个栗子(json-rules-engine)

    let a = 3;
    if (a > 2) {
      console.log('a大于2');
    } else {
      console.log('a小于等于2');
    }
    // 用规则引擎怎么写
    // 描述 if (a > 2) console.log('a大于2')
    const rule0 = {
    // 描述 if (a > 2)
    conditions: {
      all: [
        {
          fact: 'a',
          operator: 'gt',
          value: 2
        }
      ]
    },
    // 描述 console.log('a大于2')
    event: {
      type: 'console',
      params: {
        message: 'a大于2'
      }
    }
    // 描述 if (a <= 2) console.log('a小于等于2')
    const rule1 = {
    // 描述 if (a <= 2)
    conditions: {
      all: [
        {
          fact: 'a',
          operator: 'lt&equal',
          value: 2
        }
      ]
    },
    // 描述 console.log('a小于等于2')
    event: {
      type: 'console',
      params: {
        message: 'a小于等于2'
      }
    }
    

    规则写好了,怎么添加上去呢?

    import { Engine } from 'json-rules-engine';
    const engine = new Engine();
    engine.addRule(rule0);
    engine.addRule(rule1);
    

    除了添加规则,其实还能自定义一些FACT、操作符等

    engine.addFact('account-type', function getAccountType(params, almanac) {
      // ...
    })
    engine.addOperator('startsWithLetter', (factValue, jsonValue) => {
      if (!factValue.length) return false
      return factValue[0].toLowerCase() === jsonValue.toLowerCase()
    })
    

    使用 engine.run() 将规则引擎驱动起来
    可以通过 engine官方文档 查看更多用法

    实用场景:权限控制

    config.beforeEach((to, from, next) => {
      (async() => {
        try {
          // 获取当前用户信息
          const user = await getCurrUserInfo();
    
          if (to.path.startsWith('\/a')) {
            // 只有管理员可以打开 a 页面
            if (user.isAdmin()) {
              next();
            } else {
              next({ name: 'Denied', message: '您不是管理员无法打开此页面' });
            }
          } else if (to.path.startsWith('\/b')) {
            // 团队1、2、3 内成员可以打开 b 页面
            if (
              user.isMemberOf('团队1') ||
              user.isMemberOf('团队2') ||
              user.isMemberOf('团队3')
            ) {
              next();
            } else {
              next({ name: 'Denied', message: '您不是团队1、2、3中任意一个团队的成员,无法打开此页面' });
            }
          } else if (to.path.startsWith('\/c')) {
            // 团队C内成员,可以打开处于可用状态的 c 页面
            const c = await getInfoForWarPage();
            if (
              user.isMemberOf('团队C') &&
              c.isAble()
            ) {
              next();
            } else {
              next({ name: 'Denied', message: '您不是团队C的成员,或 c 页面当前禁用,无法打开该页面' });
            }
          } else {
            // 没有做权限控制的页面
            next({ name: 'Denied' });
          }
        } catch (err) {
          next({ name: 'Denied' });
        }
      })();
    });
    

    从上述代码可以看出其拓展性差,if 嵌套深,功能杂糅。现在可以根据引擎模板的思路改造一下:
    FACT和规则集合:to.path.startsWith('\/a')to.path.startsWith('\/b')to.path.startsWith('\/c')user.isAdmin()user.isMemberOf('团队1') || user.isMemberOf('团队2') || user.isMemberOf('团队3')user.isMemberOf('团队C') && c.isAble()
    推论:{ name: 'XXX', message: 'XXX' }(next()是vue-router的用法)

    // oper/starts_with.js'
    // 操作符 operate
    export function operStartsWith(factValue, jsonValue) {
      return factValue.startsWith(jsonValue);
    }
    
    // fact/is_admin.js'
    // 异步事实 fact 可以通过 [almanac官方文档](https://github.com/CacheControl/json-rules-engine/blob/master/docs/almanac.md) 查看更多用法
    const user = await getCurrUserInfo();
    export async function factIsAdmin(params, almanac) {
      const user = await almanac.factValue('user');
      return user.isAdmin();
    }
    
    // fact/is_member_of.js'
    const user = await getCurrUserInfo();
    export async function factIsMemberOf(params, almanac) {
      const teams = params.teams || [];
      const user= await almanac.factValue('user');
      return teams.some(team => user.isMemberOf(team));
    }
    
    // fact/is_c_able.js'
    const user = await getCurrUserInfo();
    export async function factIsCAble(params, almanac) {
      const c = await getInfoForCPage();
      return c.isAble();
    }
    
    // rule/a.js'
    const ruleFoo0 = {
      conditions: {
        all: [
          {
            fact: 'path',
            operator: 'startsWith',
            value: '/a'
          },
          {
            fact: 'isAdmin',
            operator: 'equal',
            value: true
          }
        ]
      },
      event: {
        type: 'auth',
        params: {
          perm: true
        }
      }
    }
    const ruleFoo1 = {
      conditions: {
        all: [
          {
            fact: 'path',
            operator: 'startsWith',
            value: '/a'
          },
          {
            fact: 'isAdmin',
            operator: 'notEqual',
            value: true
          }
        ]
      },
      event: {
        type: 'auth',
        params: {
          perm: false,
          msg: '您不是管理员无法打开此页面'
        }
      }
    }
    
    // rule/b.js'
    
    const ruleB0 = {
      conditions: {
        all: [
          {
            fact: 'path',
            operator: 'startsWith',
            value: '/b'
          },
          {
            fact: 'isMemberOf',
            params: {
              teams: [
                '团队1',
                '团队2',
                '团队3'
              ]
            },
            operator: 'equal',
            value: true
          }
        ]
      },
      event: {
        type: 'auth',
        params: {
          perm: true
        }
      }
    }
    const ruleB1 = {
      conditions: {
        all: [
          {
            fact: 'path',
            operator: 'startsWith',
            value: '/b'
          },
          {
            fact: 'isMemberOf',
            params: {
              teams: [
                '团队1',
                '团队2',
                '团队3'
              ]
            },
            operator: 'notEqual',
            value: true
          }
        ]
      },
      event: {
        type: 'auth',
        params: {
          perm: false,
          msg: '您不是团队1、2、3中任意一个团队的成员,无法打开此页面'
        }
      }
    }
    
    // rule/c.js'
    const ruleC0 = {
      conditions: {
        all: [
          {
            fact: 'path',
            operator: 'startsWith',
            value: '/c'
          },
          {
            fact: 'isMemberOf',
            params: {
              teams: [
                '团队C'
              ]
            },
            operator: 'equal',
            value: true
          {
            fact: 'isCAble',
            operator: 'equal',
            value: true
          }
        ]
      },
      event: {
        type: 'auth',
        params: {
          perm: true
        }
      }
    }
    const ruleC1 = {
      conditions: {
        all: [
          {
            fact: 'path',
            operator: 'startsWith',
            value: '/c'
          },
          {
            fact: 'isMemberOf',
            params: {
              teams: [
                '团队C'
              ]
            },
            operator: 'notEqual',
            value: true
          },
          {
            fact: 'isCAble',
            operator: 'notEqual',
            value: true
          }
        ]
      },
      event: {
        type: 'auth',
        params: {
          perm: false,
          msg: '您不是团队C的成员,或 c 页面当前禁用,无法打开该页面'
        }
      }
    }
    
    // 入口
    import operStartsWith from './oper/starts_with';
    import factIsAdmin from './fact/is_admin';
    import factIsMemberOf from './fact/is_member_of';
    import factIsCAble from './fact/is_c_able';
    import { ruleFoo0, ruleFoo1 } from './rule/a';
    import { ruleBar0, ruleBar1 } from './rule/b';
    import { ruleWar0, ruleWar1 } from './rule/c';
    
    async function getPerm (to, from) {
      const engine = new Engine();
    
      engine.addOperator('startsWith', operStartsWith);
      engine.addFact('isAdmin', factIsAdmin);
      engine.addFact('isMemberOf', factIsMemberOf);
      engine.addFact('isCAble', factIsCAble );
      engine.addRule(ruleA0);
      engine.addRule(ruleA1);
      engine.addRule(ruleB0);
      engine.addRule(ruleB1);
      engine.addRule(ruleC0);
      engine.addRule(ruleC1);
    
      const user = await getCurrUserInfo();
      const ret = await engine.run({ user, path: to.path })
    
      if (ret.events.length) {
        return ret.events[0].params;
      } else {
        return { perm: false, msg: '没有做权限控制的页面禁止打开' }
      }
    }
    
    // router.config.js
    import getPerm from './auth/index.js'; // 入口
    config.beforeEach((to, from, next) => {
      try {
        const ret = await getPerm(to, from);
    
        if (ret.perm) {
          next();
        } else {
          next({
            name: 'Denied',
            params: {
              title: '访问受限',
              message: ret.msg || '请检查您的权限以确保可以打开此页面'
            }
          });
        }
      } catch (err) {
        console.error(err);
        next({
          name: 'Denied',
          params: {
            title: '访问受限',
            message: `发生未知错误 ${err.message}`
          }
        });
      }
    });
    

    之后新增路由只需要新增其对应的文件以及规则即可,后续如果后台支持的情况下,这些配置文件可直接入库(JSON格式),前端需要调整的地方不多,可延展性好,当然还有改进的点。。。

    补充:priority 对于提升规则匹配的性能十分显著。
    如果没有设置 priority,所有规则都相当于一个个微任务,并发执行。当设置了 priority,那么相同 priority 的规则会组合成一个新的队列,根据优先级执行。因此将重要的规则排在前面,就可以很好的优化性能

    参考文档:json-rules-engine

    相关文章

      网友评论

          本文标题:规则引擎

          本文链接:https://www.haomeiwen.com/subject/mldtertx.html