谈到引擎可能有点生疏,还是拿程序员比较好理解的触发器举例吧。
数据库触发器大家应该都比较熟悉了,其背后体现的就是一个简单的规则引擎。
即:满足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,
此处能不能统一函数调用和事件通知呢?
考虑操作者的使用页面,想指定某个规则,如温度>10,
是不是应该先选择要操作的对象,如human还是device,
选择了业务对象,不同的业务对象具备不同的条件选择
如果仅仅是瞬时数据判断还好,上报的数据填充到Message中,通过mq
或者socket
通知规则引擎程序
如果是过程数据判断,或者状态数据,就比较麻烦:
一个设备连续最近6个点数据小于100,则提醒用户,
像这种需求,就依赖于程序中有每个设备对象的连续数据,当然主动查询也可以,但是效率就低效了
我猜想的应用是每个业务实例对象,都在内存/redis中保持了自身的状态数据,不管是档案类还是数据类
每次收到mq
或socket
的事件触发时,都要 1. 更新内存库 2. 执行rule逻辑
0. 所有业务对象需要判断的数据(含档案)都保存在内存库中
1. 所有的事件触发都用于更新内存库
2. 所有的条件判断都是基于内存库进行判断
事件的触发是由谁来确定的呢?
如:A.温度>100且B.状态是运行时执行某动作
有以下几个时间点,需要事件触发判断
1. 系统启动时,A、B两个业务对象初始化后,(一般是主动查询)
2. A温度数据上报后
3. B状态上报后
步骤2、步骤3我们都认为是通过mq/socket触发通知的,如果是mq,那么要指定topic
所以要监听的事件的触发,依赖于条件的选择,而条件的选择又依赖于具体的业务对象
- 1) 一般场景
A温度传感器 >= 18度,则开启空调
-
- 适配
ifttt
场景
- 适配
当 任意类型的 设备 ,创建时,通知XX用户
当 指定的设备A 更新时,通知YY用户
其中 创建时、更新时,更像切面编程在某个方法的执行前后执行某些动作,
有没有必要做这种类似于方法执行前后的切面拦截呢?
能想到的一个可用场景就是当设备上线时,自动建档
如果可以通过规则引擎来实现,就省去了编码的工作,只通过配置就能达到目的
当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()
{
}
}
如果选择单体部署,消息的传递都是通过方法的调用来传递的,参见观察者模式
如果是服务部署,消息的传递是通过socket
或者消息中间件 传递
如何提取公共接口,方便单体和服务两种方式可以做到随意切换。
联想到dubbo
的架构,其实就是观察者模式在多机器 的体现,观察者模式主要是用于事件驱动,传递消息的
dubbo
中的服务配置中心其实就是充当了 consumer
的主题发布角色,consumer
就是观察者
总结:
condition
确定了页面可选择的监测项,检测项确定了topic
,即condition确定了topic
condition
是和具体的业务对象相关的
业务对象的切入点有字段和方法,使用内存数据库来判断多个字段/数据的状态,使用aop
切面决定方法的监测
看了一些开源框架,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() {
}
}