常變業務代碼整理及規則引擎實踐

前言

現代編程日益複雜,面臨如下問題
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,使用規則引擎將會在中長期維護中得到好處
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章