规则引擎

2022-04-13  本文已影响0人  buffonme

什么是规则引擎

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

规则引擎:整合了传入系统的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

上一篇下一篇

猜你喜欢

热点阅读