前言
現代編程日益複雜,面臨如下問題
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,使用規則引擎將會在中長期維護中得到好處