规则引擎

什么是规则引擎

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容