再见了 ! if-else !拥抱规则引擎

分享是一种精神,是加深理解最好的方式之一

前言

现代编程日益复杂,面临如下问题
1、为提高效率,管理流程必须自动化,即使现代商业规则异常复杂。
2、市场要求业务规则经常变化,IT系统必须依据业务规则的变化快速、低成本的更新。
3、为了快速、低成本的更新,业务人员应能直接管理IT系统中的规则,不需要程序开发人员参与

插曲


世界上最遥远的距离,是我在if里你在else里

  • 我们势必都曾经经历过这样的场景
    刚开始自己写的代码很简洁,逻辑清晰,函数精简,没有一个if-else
    但是需求发展却不以人的意志为转移

  • 业务逻辑千奇百怪

  • 产品经理脑洞大开

  • 项目进度越来约紧

落地到具体实现只能盲目的、不停地 if-else,渐渐地,代码变得越来越庞大,继续维护起来 想吐!

  • 代码逻辑复杂,维护性差
  • 可读性差
  • 修改风险极高

回头反思一下, if-else不外乎以下若干场景

  • 异常逻辑处理
  • 特殊case
  • 不同流程状态

owner精神:如果if-else难以避免,如何正视它?

1、尽可能的合并各种条件分支

  • 重构前
 if(条件A) {
  methodX()
 }
  中间隔着N多行代码
..........
 if(条件B) {
  methodX()
 }

通常以下场景会出现如上代码

  • 维护他人代码,图简单省事
  • 需求变更,随意的if-else:忽略了上下文的逻辑or语义关系
重构后
if(条件A || 条件B ){
  methodX()
}

2、减少if-else嵌套

  • 糟糕的代码
if(条件A){
   methodA();
  .................
}else{
   if (条件B){
      methodB();
    }else{
     .................
      if (条件C){
        methodC();
      }
   }
}

代码嵌套了三层,会把自己和接锅的人绕晕!
其实嵌套if-else和外层业务逻辑并无关联性,完全可以提取到最外层,if-else互斥,尽量避免包含从属关系
if-else 最好是互斥关系!

重构后
if(条件A){
  methodA();
}
if(条件B){
  methodB();
}
if(条件C){
  methodC();
}

3、彻底分离异常流程和主干流程

重构前

if(result!=null){
    code=result.get("code")
    if(code.equals("200"){
        data=result.get("data")
        if(data.get("flow")!=null){
           //处理流水信息

        }else {
           log.error("未抓取流水信息,tid:{},data:{}",tid,data)
        }
     }else{
        log.error("获取数据失败,code:{} , msg :{} ",code,msg )
  }else{
       log.error("http请求失败")
  }
    
} 
  • 异常流程和主干流程交织在一起,二者职责模糊
  • 代码量大的情况逻辑混乱,阅读难度高
重构后
if(result==null){
     log.error("http请求失败");
     return;
}
if(! code.equals("200"){
      log.error("获取数据失败,code:{} , msg :{} ",code,msg )
      return;
}
 data=result.get("data")
if(data.get("flow")!=null){
    log.error("未抓取流水信息,tid:{},data:{}",tid,data);
    return
}

// TO DO  处理流水

tip 实际业务中逻辑远比上述demo复杂
同学们一定避免让if-else参与过多的异常流程处理

4、if-else避免条件范围过大

if(district=='南区' ){
    //TO DO
    //...........
    if(companyid='ningbo' || companyid='hangzhou'){
       //业务逻辑处理
   }
}

*tip 实际处理的只是宁波和杭州两家分公司的业务,但是if的条件却是整个南区,if 条件分支处理尽可能的缩小范围
比如公积金社保json数据出现新case,尽量缩小case的范围,比如新case解析特殊方法加上条件判断

  • 业务逻辑,特殊case处理和常规流程要通过 if-else 区分开来
//常规流水解析
commonFlowParse()
//特殊case解析
if(orgid='gjj_ningbo' || orgid= 'gjj_zhengzhou'){
  //处理特殊case
}

5、if-else内的代码提取和封装成方法

伪代码:略

ps :设计模式大法替代if-else

  • 策略模式:多种子类策略实现类代替新的if分支
  • 模板方法:子类整合一系列的接口实现方法,替代某一类型的if-else
  • 装饰器:增加新的子类装饰器链,替代新的if-else 增强方法
  • 工厂方法:不同的工厂方法,生产不同的实例,替代if-else
  • .....
    ps :设计模式充分利用Java多态,实现上通过继承和组合,会额外创建很多类对象,类与类,模块之间关系更为复杂!
    在需求多变,毫无套路和章法的情况下使用,容易出现设计过度,矫枉过正....


规则引擎是什么

你可能仍然对为什么使用规则而感到困惑?如果只是一个或几个逻辑判断,确实没有必要使用规则引擎,if-else 或者硬编码 可以更好地满足我们的需求。然而,业务规则往往是一个庞大且不断变化的规则组合,这使得系统非常复杂,如果只是使用常规代码,则会产生大量的维护工作

规则引擎应用场景

  • 流程分支非常复杂,规则变量庞大,常规编码(if-else)难以实现
  • 有不确定性的需求,变更频率较高
  • 需要快速做出响应和决策
  • 规则变更期望脱离于开发人员,脱离coding

规则引擎流程

Drools 规则引擎基于 ReteOO 算法(对面向对象系统的Rete算法进行了增强和优化的实现),它将事实(Fact)与规则进行匹配,然后交给引擎去执行,将业务规则从应用程序代码中分离出来

  • 规则引擎实施前后


  • 业务规则发展历程


业务需求

某平台内部验证码打码路由策略

规则因子如下

  • platform : 101打码 102打码 202打码兔 203云速打码....
  • site : boc cittcc 云速打码 hangzhou_gjj hefei_sb 101打码
  • type:中文 打码兔 6位数字 102 算术题 云速
  • rate : sz_sb正确率 >= 50% 10打码 否则 云速
  • auto_retrys :2次以上 打码兔
  • all_retrys:3次以上 直接抛异常
  • appid : kuaidai 小费打码

伪代码

- 结合配置文件
if( appid== kuaidai){
  小费打码
}
if(orgid =='wuhan_gjj' ||  tianjin  nanjing  wenzhou .....){
   云速打码
}ese if(orgid=='anhui_10086' || chengdu  hefei ){
   小费打码
}  
.......
if(platform=101){
   if( auto_retrys>2 || rate <50%   ){
      云速打码 
  }
  if(type== 中文||.....) {
     打码兔
 }
 .................  
}else if(platform== 202){
 打码兔
}
if( (orgid== suzhou_gjj || ningbo....) && all_retrys >1){
  云速打码
}
if( all_retrys>3){
   抛异常.....
}

业务痛点

1、目标网站验证码改版:比如杭州社保验证码是中文,刀哥完全不支持,需要紧急转移到小费打码,然后继续观察成功率,继续视情况而定再次迭代切换
2、机器学习训练集效果不错:温州,宁波社保小费打码校正完成了,成功率提升,外部打码可以切换过去
3、监控预警:grafana监控显示中国银行boc小费打码正确率只有10%,需要快速切换到云速打码
4、 临时需求:央行征信验证码小费或者偃月刀打码重试次数超过1次,转到外部打码
5、贝多多新商户接入验证码打码:API接口价格还未,定暂不使用外部打码,全部小费打码,然后观察监控
6、打码兔账户没钱了,紧急转移到云速打码
7、 外部打码花钱如流水:全部切换到小费,然后继续观察
新的规则因子不断在增加.....
......................

  • 上述需求,变动频率高
  • if-else越来越长 直到写不下去
  • 即使是小改动也需要经常重启系统

规则引擎drools如何解决

1、创建fact对象,设置规则因子
        Router router=new Router();
        //客户端调用入参,可以为空,下同
        router.setAppid("贝多多appid");
        router.setPlatform("101");
        router.setSite("ningbo_gjj");
        router.setTypeid("42");
        //根据自动打码的tid重试次数计算得来
        router.setAutoRetrys(3);
        //根据打码的tid重试次数计算得来
        router.setAutoRetrys(1);
       //基于hashmap统计得来的
        router.setRate(0.5F);

这是一种典型的OO思想,打码路由策略不再是复杂的if-else流程分支,而是去生成路由策略所需要的规则因子,构造Fact*JavaBean对象然后交给规则引擎去执行。

2、生成规则-drl数据文件
package router;
import com.xu.rules.dataobject.entity.Router;

rule "贝多多打码"
    salience 100
    date-expires "09-五月-2018"
    no-loop true

    when
        $router : Router(appid.equals("beiduoduo"));
    then
        System.out.println("贝多多执行优先内部打码!");
        $router.setResult("偃月刀打码.");
end


rule "云速打码"
    salience 200
    no-loop true
    when
       $router : Router( yunsuSite() contains site);
    then
        $router.setResult("云速打码.");
end
 
 
function String yunsuSite() {

   String sites="nanjing_gjj,hangzhou_sb,tianjin_gjj,gz_10086@pc";
   return sites;
}


  
3、规则文件预加载

drl文件需要先加载到drools工作内存,也就是加载到drools的容器中

        KieServices kieServices = getKieServices();
        
        final KieRepository kieRepository = kieServices.getRepository();

        kieRepository.addKieModule(() -> kieRepository.getDefaultReleaseId());

        KieBuilder kieBuilder = kieServices.newKieBuilder(所有的规则文件);
        Results results = kieBuilder.getResults();
        if (results.hasMessages(Message.Level.ERROR)) {
            // 验证drl规则文件的合法性
            System.out.println(results.getMessages());
            throw new IllegalStateException("### errors ###");
        }
        //构建规则文件
        kieBuilder.buildAll();
        //最终得到一个规则引擎容器
        KieContainer kieContainer = kieServices.newKieContainer(kieRepository.getDefaultReleaseId());

KieServices:drools的管理中心API,提供了CRUD,构建,管理和执行接口
kieRepository :管理规则的知识仓库
KieContainer:管理容器
.......

4、fact对象 碰撞 ”规则“
          //构建规则因子
          Router router = new Router();
          router.setAppid(param.getAppid());
          router.setPlatform(param.getPlatform());
          router.setSite(param.getSite());

          //获取session
          KieSession kieSession = kieContainer.newKieSession();
          //碰撞规则-插入进去
          kieSession.insert(router);
          // 执行 返回得到 命中的规则数量
          int rules = kieSession.fireAllRules();
          //资源释放
          kieSession.dispose();

ps : 实际工作中,可以把规则引擎drl文件放在db中去维护,规则变更后,直接修改db,然后动态加载规则到drools的工作内存,系统无需重启,规则即时生效!规则引擎宿主机多实例的情况下,可以通过消息中间件消息订阅的形式,通知到所有的实例重载规则!

截止到上述介绍,规则引擎drools差不多已经可以解决我们复杂业务规则流程多变的系统,但是我们可以更进一步,把以上规则变更的锅 扔给业务人员。

drools-决策表

通过应用规则引擎,将规则引擎中的决策表和Excel结合起来,将Excel数据文件直接导入到规则引擎的决策表中,然后决策表以规则的方式存储在规则库管理系统中。
Excel通过规则引擎中的规则包进行分门别类的方式保存,同时跟随规则包一起形成可追溯的规则版本,以便在需要的时候进行追溯查看

  • 验证码路由-决策表


  • 决策表-原理
    决策表-xls文件实质还是drl文件,规则引擎执行过程中,需要把excel文件翻译成drl文件,然后加载到内存

            InputStream inputStream = new FileInputStream(excel);
            //excel文件解析成drools的需要的格式
            SpreadsheetCompiler compiler = new SpreadsheetCompiler();
            Resource resource = ResourceFactory.newInputStreamResource(inputStream, "UTF-8");
             //最终得到规则文件drl的字符串
            String rules = compiler.compile(resource, "rule-table");
            //调取上述load方法,加载到工作内存
  • 决策表的出现,很大程度上可以减轻IT人员的负担,把一部分频繁更新的规则因子交给业务人员去维护
  • 决策表基本语法
参数名 说明
RuelSet 在这个单元的右边单元中包含ruleset的名称 和drl文件中的package 是一样
CONDITION 指明该列将被用于规则条件 CONDITION (代表条件) 相当于drl中的when
ACTION 指明该列将被用于推论,简单理解为结果 相当于drl中r then ACTION 与CONDITION 是平行的
PRIORITY 指明该列的值将被设置为该规则行的'salience'值
RuleTable 规则名,写法是 在RuleTable后直接写规则名的前缀,不用另写一列

.....

更为复杂的业务场景

  • 手机运营商资费套餐
  • 超市、商场,商城等等积分计算规则
  • 寿险车险理赔
  • 工资计算(ScriptEngine)
    PS:如果我们应用的生命周期很短,也没有必要使用Drools,使用规则引擎将会在中长期维护中得到好处。

三克油

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • 规则引擎 相关介绍 规则引擎起源于基于规则的专家系统,而基于规则的专家系统又是专家系统的其中一个分支。专家系统属于...
    xiaolyuh阅读 22,730评论 1 12
  • ———— 暮惜归 青砖绿瓦一壶杏花 浸透乡村淡饭粗茶 屋里素手绣服静雅 田间黄牛老犁吱呀 何时才...
    暮惜归阅读 87评论 0 0
  • 上午小组总结: 关于收获: 面试时考查: Java基础 逻辑思维 读代码能力 写代码能力(编程题) 算法题 学习过...
    hongXkeX阅读 388评论 0 0
  • 当年你来到我身旁, 连衣角都有黄昏的毛边。 你的发是黄昏的, 睫毛是黄昏的, 脖颈处的细微绒毛是黄昏的, 甚至你捧...
    惟西风恨阅读 171评论 2 7