hahasansan / rule-engine

0 stars 0 forks source link

需求大纲 #3

Open RovingSea opened 6 months ago

RovingSea commented 6 months ago

一、应用背景

上层业务中,往往会出现校验的场景,例如有如下场景: 以社交软件的用户交流、用户更换名称和头像为例:

如果以强耦合的做法会导致业务代码中出现冗余的校验逻辑, 例如更换用户信息时,对新名称进行校验的代码:

private boolean updateUserInfo(User user) {
    // 是否含有非法符号
    if (containsIllegalSymbol(user.getName())) {
        return false;
    }
    // 是否含有不良单词
    if (containsBadWords(user.getName())) {
        return false;
    }
    // 更多校验 more validations ...

    userService.save(user);
    return true;
}

这么做会有如下几个缺点:

  1. 维护性差和可扩展性差:每次新增/删除校验规则都需要修改业务代码,违反开闭原则。
  2. 可读性差:业务代码中充斥着大量的校验逻辑,降低了代码的可读性。

因此,这是不可取的。

从软件的设计角度出发,校验逻辑不应该耦合于业务逻辑中,简言之, 需要满足每次新增/删除校验规则都不用修改业务代码

因此,需要一款业务组件,既可以帮助业务代码摆脱耦合校验逻辑,也可以 灵活地配置规则的数目和逻辑。

至于有哪些规则?校验的内容是什么?在业务代码是无感知无需关心的。 如下代码:

private boolean updateUserInfo(User user) {
    ruleEngine.addModel(RuleModel.USER, user);
    RuleResult result = ruleEngine.run(VerificationAction.CHANGE_USERINFO);
    if (!result.isSuccess()) {
        return false;
    }

    userService.save(user);
    return true;
}

上述代码中,更换名称的业务代码中传递了一个枚举VerificationAction.CHANGE_USERINFO 和一个校验模型 User 给到规则引擎,再通过规则的校验结果来决定是否成功。

无论如何新增/删除校验规则都不需要更改方法

private boolean updateUserInfo(User user)

内的逻辑。这样就满足了开闭原则,同时提高了代码的可读性。

二、需求内容

(一)规则定义

已知有如下要求:

  1. 配置化,在不需要更改代码的情况下,可以控制一个枚举对应有哪些规则;
  2. 热更新,在不需要重启服务的情况下,可以更新规则的代码逻辑。

因此可以带入脚本的思想,将规则的代码作为待执行的脚本放到库中存储, 紧接着规则引擎将这些这些待执行的脚本作为 spring bean,随用随取。

在此,优先推荐 groovy 脚本文件。

除此外,规则应该还包含唯一标识,名称,描述,脚本类型(暂时只完成 groovy)

(二)规则引擎可以通过标识确认要执行的规则

根据上述案例,业务代码中给规则引擎传递了一个枚举VerificationAction.CHANGE_USERINFO 和一个校验模型 User

首先,我们需要确定要执行的规则有哪些。

在此之前,我们可以提出一个定义,多个规则的集合称之为规则组, 可以以上层业务传递的枚举的code作为规则组的唯一标识。

又因为一个规则组可能会有多个规则,而一个规则可能也会存在于多个规则组中,如:

所以规则组与规则建立的是多对多关系, 因此,需要衍生新表,也就是规则关联表。

在关联表中可以通过规则组 code 确定其要执行的规则有哪些。

总结,通过枚举VerificationAction.CHANGE_USERINFO确定要执行的规则。

(三)校验对象

在(二)中,我们分析了枚举VerificationAction.CHANGE_USERINFO的作用,

接下来我们来分析如何将 User 放到规则中进行校验。

首先,我们可以参考 IOC(控制反转) 的思想,我们在使用 Spring 框架时, 对象的创建和对象之间的调用过程,是交给Spring管理的, 我们只是使用了注解进行定义,即可在Spring容器的中获取到被注解标注的Bean

那么,我们也可以参考这种做法,把要校验的对象视为Bean,然后我们自己再写一个上下文, 需要满足以下功能:

  1. 手动推校验对象到上下文中,即上述案例代码中的
    ruleEngine.addModel(user);
  2. 约定化。扫描规则引擎提供的注解@RuleModel, 凡是上层在方法上标注了@RuleModel的,将该方法的返回结果自动装配到上下文中

完成了校验对象如何装配到上下文中,还需要再完成一步。

就是规则内容中如何获取这个校验对象。

我们可以让所有规则实现一个接口RuleProcessor,代码如下:

public interface RuleProcessor {

    RuleResult run(RuleProcessContext context) {

    } 

}

在方法run的入参中,从context获取要校验的对象,如校验用户名称是否含有非法符号, 代码如下:


public class Rule001 implements RuleProcessor {

    @Override
    RuleResult run(RuleProcessContext context) {
        User user = context.getModel(RuleModel.USER, User.class);
        if (containsIllegalSymbol(user.getName())) {
            return RuleResult.fail("用户名称中含有非法符号");
        }
    }

}

注意,这个代码是在脚本中完成,以 Groovy 完成,代码内容存在规则中,见(一)规则定义。

总结,通过 IOC(控制反转) 的思想,将编写规则的动作脱离业务代码,上层可以通过 手动推校验对象,也可以通过约定的方式确认校验对象。

(四)如何获取规则

在(二)的理论基础上,我们可以定义三张表,规则组、规则、规则关系表, 我们通过规则组 code,先在规则关系表中查询出该规则组下有多少规则, 在通过每一个规则 code,查询出每一个规则的脚本内容。

(五)简化上层同步规则步骤

上层在选定的目录下编写 Groovy 脚本,并可以通过调用规则引擎服务的接口,完成同步功能。 目的是方便上层在开发时,可以在idea中编写脚本,而不需要切换开发工具到数据库中将规则内容进行更新。

三、整体流程时序图

将下述代码粘贴到此处,所见即所得

@startuml
participant 上层业务
autoactivate on

上层业务 -> 规则引擎 #005500: 规则组Code

规则引擎 -> 数据库: 规则组 Code
return 规则集

规则引擎 -> 规则引擎: 初始化上下文
return

规则引擎 -> 规则引擎: 将规则集脚本作为 Bean 注入 Spring 容器中
return

规则引擎 -> 规则引擎: 执行规则
return 结束

规则引擎 -> 上层业务: 执行结果

@enduml
hahasansan commented 5 months ago

@startuml participant 上层业务 autoactivate on

上层业务 -> 规则引擎 #005500: 规则组Code

规则引擎 -> 数据库: 规则组 Code return 规则集

规则引擎 -> 规则引擎: 初始化上下文 return

规则引擎 -> 规则引擎: 将规则集脚本作为 Bean 注入 Spring 容器中 return

规则引擎 -> 规则引擎: 执行规则 return 结束

规则引擎 -> 上层业务: 执行结果

@enduml