Skip to content

Latest commit

 

History

History
266 lines (153 loc) · 7.65 KB

规则引擎.md

File metadata and controls

266 lines (153 loc) · 7.65 KB

一. 规则引擎是什么

谈到引擎可能有点生疏,还是拿程序员比较好理解的触发器举例吧。

数据库触发器大家应该都比较熟悉了,其背后体现的就是一个简单的规则引擎。

即:满足X条件,执行y动作。

二. 思考实现

自然会想到有一个对象,表示规则,包含条件和对象,

条件包含一个计算方法,执行时可能有一些变化的、动态生成的参数作为计算的依据,所以要暴露一个参数,作为数据的载体。

动作包含一个执行方法,动作需要参数吗,有可能要根据条件判断的过程中额外产生的数据来执行不同的动作

Message本质上就是一个key-value的容器

class Rule
{
    Condition c;
    List<Action> a;
}

interface Condition
{
    Boolean evaluate(Message message);
}

interface Action
{
    Boolean execute(Message message);
}

要明确下一件事,我们现在要考虑的数据产生、规则处理都不是在一个进程中,这也符合大都数实际应用。

所有事件的触发就不能仅仅采用方法调用来实现。

所以Rule启动时,就必须让不同的子类进行不同的初始化:

如基于Socket自定义协议的需要自己开启端口处理后续逻辑;

基于Mq中间件的需要订阅相应的Topic,

此处能不能统一函数调用和事件通知呢?

1. 规则是和业务深度绑定的

考虑操作者的使用页面,想指定某个规则,如温度>10,

是不是应该先选择要操作的对象,如human还是device,

选择了业务对象,不同的业务对象具备不同的条件选择

2. 应用中拥有一个类似内存数据库的存在

如果仅仅是瞬时数据判断还好,上报的数据填充到Message中,通过mq或者socket通知规则引擎程序

如果是过程数据判断,或者状态数据,就比较麻烦:

一个设备连续最近6个点数据小于100,则提醒用户

像这种需求,就依赖于程序中有每个设备对象的连续数据,当然主动查询也可以,但是效率就低效了

我猜想的应用是每个业务实例对象,都在内存/redis中保持了自身的状态数据,不管是档案类还是数据类

每次收到mqsocket的事件触发时,都要 1. 更新内存库 2. 执行rule逻辑

0. 所有业务对象需要判断的数据(含档案)都保存在内存库中
1. 所有的事件触发都用于更新内存库
2. 所有的条件判断都是基于内存库进行判断

3. 事件的触发时机

事件的触发是由谁来确定的呢?

如:A.温度>100且B.状态是运行时执行某动作

有以下几个时间点,需要事件触发判断

1. 系统启动时,A、B两个业务对象初始化后,(一般是主动查询)
2. A温度数据上报后
3. B状态上报后

步骤2、步骤3我们都认为是通过mq/socket触发通知的,如果是mq,那么要指定topic

所以要监听的事件的触发,依赖于条件的选择,而条件的选择又依赖于具体的业务对象

4. 哪些地方可以使用规则引擎

  • 1) 一般场景

A温度传感器 >= 18度,则开启空调

    1. 适配 ifttt场景

当 任意类型的 设备 ,创建时,通知XX用户

当 指定的设备A 更新时,通知YY用户

其中 创建时、更新时,更像切面编程在某个方法的执行前后执行某些动作,

有没有必要做这种类似于方法执行前后的切面拦截呢?

能想到的一个可用场景就是当设备上线时,自动建档

如果可以通过规则引擎来实现,就省去了编码的工作,只通过配置就能达到目的

5. 和代码的映射

当A设备温度大于29时,开启B

实际监测的是 设备实例(业务对象)字段

当设备新建时,通知用户

实际监测的是 方法,从方法的角度来看,监测方法更像是AOP切面拦截

当A设备连续3个点数据<20 ,执行XX

过程数据就是 时间 + 字段

如何实现呢,猜想如下:

class Device
{
    /**
    * 界面选择“设备”,选择“任一设备还是指定设备“--->下拉框选择”温度A、温度B“-->选择操作符”》=《",选择基准值
    * 这种判断主要有三要素:who(tmpA)+表达式(><=!=)+基准值,
      只要将三要素转化为条件表达式就可以了
    **/
    @ConditionUse("温度A","topicA")
    public int tmpA;
    
    @ConditionUse("温度A","topicB")
    public int tmpB;

    @Before("保存前","beforeSave",还需要Message传递保存前能获取的信息)
    @After("保存后","afterSave",还需要Message传递保存后能获取的信息)
    public void save()
    {

    }
}

6. 单体部署和服务部署

如果选择单体部署,消息的传递都是通过方法的调用来传递的,参见观察者模式

如果是服务部署,消息的传递是通过socket或者消息中间件 传递

如何提取公共接口,方便单体和服务两种方式可以做到随意切换。

联想到dubbo的架构,其实就是观察者模式在多机器 的体现,观察者模式主要是用于事件驱动,传递消息的

dubbo中的服务配置中心其实就是充当了 consumer的主题发布角色,consumer就是观察者

总结:

condition确定了页面可选择的监测项,检测项确定了topic,即condition确定了topic

condition是和具体的业务对象相关的

业务对象的切入点有字段和方法,使用内存数据库来判断多个字段/数据的状态,使用aop切面决定方法的监测

三. easy-rule

看了一些开源框架,Drool太复杂,还是easy-rule简单清晰,不过功能也受限。

样例:

//指定rule及对应的条件condition和动作action
Rule rule = new RuleBuilder()
    .when(condition)
    .then(action1)
    .then(action2)
    .build();
rules.register(rule);

//注意这边是程序主动调用fire后触发,这是最大的问题
//实际部署时,如规则处理程序和数据上报程序很可能是两个不同的进程应用,很难通过方法的调用来触发
//所以easy-rule适合单体应用,这样的话何时执行(when)是由本程序控制的.

//实际上这里的fire对应着when
rulesEngine.fire(rules, facts);

To be continue...

updated 2020-09-11 am:

思考下如何让页面方便的选择:

/**
 * 像 温度、湿度等数据应该是和具体业务对象绑定的,
   所以正常情况下应该是放在业务对象 如Device中,但是实际开发过程中,
   业务对象很多都是存储其档案信息,像温度等数据信息是没有实体对象和其对应的,
   
   放在业务对象中,在开发看来只是增加了冗余信息,
   因为这个对象存在的目的就是为了规则引擎的执行,开发不关心
   
   所以新建一个类,单独存储数据信息,但是不同的业务对象的数据是不一样的
   所以添加一个注解,指明是针对哪个业务对象
   
   字段上可以添加两种信息:
   1. 该字段代表什么数据,一般可用于配置规则时,供用户选择
   2. 该字段数据来源的topic,有可能多个字段对应同一个topic,此时需要在传入的Message,通过其他参数区分
 * @author spikeF
 * @date 2020/9/4 16:38
 */
@TopicFor(Device.class)
public class MemeryDatabase {

    @ProperyDesc(value = "温度", topic = "tmpAndOther")
    private int a;

    @ProperyDesc(value = "湿度", topic = "tmpAndOther")
    public String b;

    @ProperyDesc(value = "速度", topic = "speed")
    public String c;


    public void load() {

    }

    public void loadFromDb() {

    }
}