開源規(guī)則引擎——ice:致力于解決靈活繁復(fù)的硬編碼問題
開源規(guī)則引擎——ice:致力于解決靈活繁復(fù)的硬編碼問題

背景介紹
業(yè)務(wù)中是否寫了大量的 if-else?是否受夠了這些 if-else 還要經(jīng)常變動(dòng)?
業(yè)務(wù)中是否做了大量抽象,發(fā)現(xiàn)新的業(yè)務(wù)場景還是用不上?
是否各種調(diào)研規(guī)則引擎,發(fā)現(xiàn)不是太重就是接入或維護(hù)太麻煩,最后發(fā)現(xiàn)還是不如硬編碼?
接下來給大家介紹一款全新的開源規(guī)則引擎——ice,以一個(gè)簡單的例子,從最底層的編排思想,闡述 ice 與其他規(guī)則引擎的不同;講述 ice 是如何使用全新的設(shè)計(jì)思想,契合解耦和復(fù)用的屬性,還你最大的編排自由度。
規(guī)則引擎的應(yīng)用場景
規(guī)則引擎在很多業(yè)務(wù)場景中都有應(yīng)用,例如:
會(huì)員營銷:由多種條件、流程、獎(jiǎng)勵(lì)組合而成,時(shí)間線復(fù)雜,代碼復(fù)用率不高,調(diào)整頻繁。
風(fēng)控規(guī)則:由多種條件組合并返回決策,條件量大且復(fù)雜,變動(dòng)頻繁。
數(shù)據(jù)分析:將數(shù)據(jù)通過分析師自己編排的規(guī)則產(chǎn)出想要的數(shù)據(jù),千人千面。
以上場景往往都存在一些共同痛點(diǎn):
靈活業(yè)務(wù)(變動(dòng)頻繁,時(shí)效性明顯,測試邏輯復(fù)雜)
追求靈活花里胡哨:產(chǎn)品和運(yùn)營一直在探索新鮮玩法,導(dǎo)致很多抽象出來的模塊往往扛不過兩個(gè)迭代。
今天上線又要調(diào)整:因?yàn)橐恍┡及l(fā)情況,如線上用戶參與度不高,及時(shí)調(diào)整用戶參與門檻等(當(dāng)然也可以在開發(fā)前把所有情況考慮到位,但是為了小概率事件做大量的工作,成本過高)。
研發(fā)測試心力交瘁:研發(fā)硬編碼,測試驗(yàn)證復(fù)雜重復(fù)邏輯,久而久之變的愈發(fā)疲憊。
時(shí)間線(多條時(shí)間線交織混亂)
研發(fā)編排錯(cuò)了再來:一般營銷類型的會(huì)涉及很多時(shí)間線,而在當(dāng)前,測試一個(gè)未來要上線的具有不同時(shí)間節(jié)點(diǎn)屬性的活動(dòng),硬編碼時(shí)往往由研發(fā)編排時(shí)間,測試進(jìn)行測試,但是當(dāng) bug 發(fā)生并打亂時(shí)間線時(shí),就需要重新編排時(shí)間(沒有經(jīng)歷過的不用太了解,后面會(huì)說)。
測試并行孔融讓梨:當(dāng)時(shí)間線發(fā)生沖突并有多個(gè)測試在沖突位置上并發(fā)測試,往往由測試自行協(xié)調(diào)測試順序,當(dāng)一方出現(xiàn)問題往往導(dǎo)致后續(xù)測試進(jìn)度不可控。
其他問題
依賴掛了難以為繼:測試環(huán)境為非穩(wěn)定環(huán)境,一旦依賴出了問題難免影響進(jìn)度,如何能做到簡單高效 mock?
修復(fù)數(shù)據(jù)苦不堪言:當(dāng)線上問題產(chǎn)生時(shí),受影響的客戶如何快速高效的補(bǔ)償?
開源規(guī)則引擎 ice 的設(shè)計(jì)思路
為了方便理解,設(shè)計(jì)思路將伴隨著一個(gè)簡單的充值例子展開。
舉例
X 公司將在國慶放假期間,開展一個(gè)為期七天的充值小活動(dòng),活動(dòng)內(nèi)容如下:
活動(dòng)時(shí)間:(10.1-10.7)
活動(dòng)內(nèi)容:
充值 100 元 送 5 元余額 (10.1-10.7)
充值 50 元 ? 送 10 積分 (10.5-10.7)
活動(dòng)備注:?不疊加送(充值 100 元只能獲得 5 元余額,不會(huì)疊加贈(zèng)送 10 積分)
簡單拆解一下,想要完成這個(gè)活動(dòng),我們需要開發(fā)如下模塊:

如上圖,當(dāng)用戶充值成功后,會(huì)產(chǎn)生對應(yīng)充值場景的參數(shù)包裹 Pack(類 Activiti/Drools 的 Fact),包裹里會(huì)有充值用戶的 uid,充值金額 cost,充值的時(shí)間 requestTime 等信息。我們可以通過定義的 key,拿到包裹中的值(類似 map.get(key))。
模塊怎么設(shè)計(jì)無可厚非,重點(diǎn)要講的是后面的怎么編排實(shí)現(xiàn)配置自由,接下來將通過已有的上述節(jié)點(diǎn),講解不同的規(guī)則引擎在核心的編排上的優(yōu)缺點(diǎn),并比較ice是怎么做的。
流程圖式實(shí)現(xiàn)
類 Activiti、 Flowable 實(shí)現(xiàn):

流程圖式實(shí)現(xiàn),應(yīng)該是我們最常想到的編排方式了~ 看起來非常的簡潔易懂,通過特殊的設(shè)計(jì),如去掉一些不必要的線,可以把 UI 做的更簡潔一些。但由于有時(shí)間屬性,其實(shí)時(shí)間也是一個(gè)規(guī)則條件,加上之后就變成了:

看起來也還好。
執(zhí)行樹式實(shí)現(xiàn)
類 Drool s實(shí)現(xiàn)(When X Then Y):

這個(gè)看起來也還好,再加上時(shí)間線試試:

依舊比較簡潔,至少比較流程圖式,我會(huì)比較愿意修改這個(gè)。
計(jì)劃永遠(yuǎn)趕不上變化
上面兩種方案的優(yōu)點(diǎn)在于,可以把一些零散的配置結(jié)合業(yè)務(wù)很好的管理了起來,對配置的小修小改,都是信手拈來,但是真實(shí)的業(yè)務(wù)場景,可能還是要錘爆你,有了靈活的變動(dòng),一切都不一樣了。
理想
不會(huì)變的,放心吧,就這樣,上!
現(xiàn)實(shí)
① 充值 100 元改成 80 吧,10 積分變 20 積分吧,時(shí)間改成 10.8 號(hào)結(jié)束吧(微微一笑,畢竟我費(fèi)了這么大勁搞規(guī)則引擎,終于體現(xiàn)到價(jià)值了?。?/p>
② 用戶參與積極性不高啊,去掉不疊加送吧,都送(稍加思索,費(fèi)幾個(gè)腦細(xì)胞挪一挪還是可以的,怎么也比改代碼再上線強(qiáng)吧!)
③ 5 元余額不能送太多,設(shè)置個(gè)庫存 100 個(gè)吧,對了,庫存不足了充 100 元還是得送 10 積分的哈(卒…早知道還不如硬編碼了)
以上變動(dòng)其實(shí)并非看起來不切實(shí)際,畢竟真實(shí)線上變動(dòng)比這離譜的多的是,流程圖式和執(zhí)行樹式實(shí)現(xiàn)的主要缺點(diǎn)在于,牽一發(fā)而動(dòng)全身,改動(dòng)一個(gè)節(jié)點(diǎn)需要瞻前顧后,如果考慮不到位,很容易弄錯(cuò),而且這還只是一個(gè)簡單的例子,現(xiàn)實(shí)的活動(dòng)內(nèi)容要比這復(fù)雜的多的多,時(shí)間線也是很多條,考慮到這,再加上使用學(xué)習(xí)框架的成本,往往得不償失,到頭來發(fā)現(xiàn)還不如硬編碼。
怎么辦?
讓我們看看 ice 是怎么做的?
引入關(guān)系節(jié)點(diǎn)
關(guān)系節(jié)點(diǎn)為了控制業(yè)務(wù)流轉(zhuǎn)。
AND
所有子節(jié)點(diǎn)中,有一個(gè)返回 false 該節(jié)點(diǎn)也將是 false,全部是 true 才是 true,在執(zhí)行到 false 的地方終止執(zhí)行,類似于 Java 的 &&。
ANY
所有子節(jié)點(diǎn)中,有一個(gè)返回 true 該節(jié)點(diǎn)也將是 true,全部 false 則 false,在執(zhí)行到 true 的地方終止執(zhí)行,類似于 Java 的 ||。
ALL
所有子節(jié)點(diǎn)都會(huì)執(zhí)行,有任意一個(gè)返回 true 該節(jié)點(diǎn)也是 true,沒有 true 有一個(gè)節(jié)點(diǎn)是 false 則 false,沒有 true 也沒有 false 則返回 none,所有子節(jié)點(diǎn)執(zhí)行完畢終止
NONE
所有子節(jié)點(diǎn)都會(huì)執(zhí)行,無論子節(jié)點(diǎn)返回什么,都返回 none。
TRUE
所有子節(jié)點(diǎn)都會(huì)執(zhí)行,無論子節(jié)點(diǎn)返回什么,都返回 true,沒有子節(jié)點(diǎn)也返回 true(其他沒有子節(jié)點(diǎn)返回 none)。
引入葉子節(jié)點(diǎn)
葉子節(jié)點(diǎn)為真正處理的節(jié)點(diǎn)。
Flow
一些條件與規(guī)則節(jié)點(diǎn),如例子中的 ScoreFlow。
Result
一些結(jié)果性質(zhì)的節(jié)點(diǎn),如例子中的 AmountResult,PointResult。
None
一些不干預(yù)流程的動(dòng)作,如裝配工作等,如下文會(huì)介紹到的 TimeChangeNone。
有了以上節(jié)點(diǎn),我們要怎么組裝呢?

如上圖,使用樹形結(jié)構(gòu)(對傳統(tǒng)樹做了鏡像和旋轉(zhuǎn)),執(zhí)行順序還是類似于中序遍歷,從 root 執(zhí)行,root 是個(gè)關(guān)系節(jié)點(diǎn),從上到下執(zhí)行子節(jié)點(diǎn),若用戶充值金額是 70 元,執(zhí)行流程:
[ScoreFlow-100:false]→[AND:false]→[ScoreFlow-50:true]→[PointResult:true]→[AND:true]→[ANY:true]
這個(gè)時(shí)候可以看到,之前需要?jiǎng)冸x出的時(shí)間,已經(jīng)可以融合到各個(gè)節(jié)點(diǎn)上了,把時(shí)間配置還給節(jié)點(diǎn),如果沒到執(zhí)行時(shí)間,如發(fā)放積分的節(jié)點(diǎn) 10.5 日之后才生效,那么在 10.5 之前,可以理解為這個(gè)節(jié)點(diǎn)不存在。
變化的靈活快速應(yīng)對
對于 ① 直接修改節(jié)點(diǎn)配置就可以。
對于 ② 直接把 root 節(jié)點(diǎn)的 ANY 改成 ALL 就可以(疊加送與不疊加送的邏輯在這個(gè)節(jié)點(diǎn)上,屬于這個(gè)節(jié)點(diǎn)的邏輯就該由這個(gè)節(jié)點(diǎn)去解決)。
對于 ③ 由于庫存的不足,相當(dāng)于沒有給用戶發(fā)放,則 AmountResul 返回 false,流程還會(huì)繼續(xù)向下執(zhí)行,不用做任何更改。
再加一個(gè)棘手的問題,當(dāng)時(shí)間線復(fù)雜時(shí),測試工作以及測試并發(fā)要怎么做?
一個(gè) 10.1 開始的活動(dòng),一定是在 10.1 之前開發(fā)上線完畢,比如我在 9.15 要怎么去測試一個(gè) 10.1 開始的活動(dòng)?在 ice 中,只需要稍微修改一下:

如圖,引入一個(gè)負(fù)責(zé)更改時(shí)間的節(jié)點(diǎn) TimeChangeNone(更改包裹中的requestTime),后面的節(jié)點(diǎn)執(zhí)行都是依賴于包裹中的時(shí)間即可,TimeChangeNone 類似于一個(gè)改時(shí)間的插件一樣,如果測試并行,那就給多個(gè)測試每人在自己負(fù)責(zé)的業(yè)務(wù)上加上改時(shí)間插件即可。
ice 的特性
為什么這么拆解呢?為什么這樣就能解決這些變動(dòng)與問題呢?
其實(shí),就是使用樹形結(jié)構(gòu)解耦,流程圖式和執(zhí)行樹式實(shí)現(xiàn)在改動(dòng)邏輯的時(shí)候,不免需要瞻前顧后,但是 ice 不需要,ice 的業(yè)務(wù)邏輯都在本節(jié)點(diǎn)上,每一個(gè)節(jié)點(diǎn)都可以代表單一邏輯,比如我改不疊加送變成疊加送這一邏輯就只限制在那個(gè) ANY 節(jié)點(diǎn)邏輯上,只要把它改成我想要的邏輯即可,至于子節(jié)點(diǎn)有哪些,不用特別在意,節(jié)點(diǎn)之間依賴包裹流轉(zhuǎn),每個(gè)節(jié)點(diǎn)執(zhí)行完的后續(xù)流程不需要自己指定。
因?yàn)樽约簣?zhí)行完后的執(zhí)行流程不再由自己掌控,就可以做到復(fù)用:

如圖,參與活動(dòng)這里用到的 TimeChangeNone,如果現(xiàn)在還有個(gè) H5 頁面需要做呈現(xiàn),不同的呈現(xiàn)也與時(shí)間相關(guān),怎么辦?只需要在呈現(xiàn)活動(dòng)這里使用同一個(gè)實(shí)例,更改其中一個(gè),另一個(gè)也會(huì)被更新,避免了到處改時(shí)間的問題。
同理,如果線上出了問題,比如 sendAmount 接口掛了,由于是 error 不會(huì)反回 false 繼續(xù)執(zhí)行,而是提供了可選策略,比如將 Pack 以及執(zhí)行到了哪個(gè)節(jié)點(diǎn)落盤起來,等到接口修復(fù),再繼續(xù)丟進(jìn) ice 重新跑即可(由于落盤時(shí)間是發(fā)生問題時(shí)間,完全不用擔(dān)心活動(dòng)結(jié)束了的修復(fù)不生效問題),同樣的,如果是不關(guān)鍵的業(yè)務(wù)如頭像服務(wù)掛了,但是依然希望跑起來,只是沒有頭像而已,這樣可以選擇跳過錯(cuò)誤繼續(xù)執(zhí)行。這里的落盤等規(guī)則不細(xì)展開描述。同樣的原理也可以用在 mock 上,只需要在 Pack 中增加需要 mock 的數(shù)據(jù),就可以跑起來。
引入前置節(jié)點(diǎn)

上面的邏輯中可以看到有一些 AND 節(jié)點(diǎn)緊密綁定的關(guān)系,為了視圖與配置簡化,增加了前置(forward)節(jié)點(diǎn)概念,當(dāng)且僅當(dāng)前置節(jié)點(diǎn)執(zhí)行結(jié)果為非 false 時(shí)才會(huì)執(zhí)行本節(jié)點(diǎn),語義與 AND 相連的兩個(gè)節(jié)點(diǎn)一致。
Talk is cheap. Show me the code…
github:https://github.com/zjn-zjn/ice
gitee:https://gitee.com/waitmoon/ice
歡迎大家使用體驗(yàn)開源的規(guī)則/流程引擎 ice。如果有遇到問題,歡迎提 issue 來交流。大家也可以添加作者微信:lwaitmoonl ,備注“ice”,進(jìn)入交流群。
Dev for Dev專欄介紹
Dev for Dev(Developer for Developer)是聲網(wǎng)Agora 與 RTC 開發(fā)者社區(qū)共同發(fā)起的開發(fā)者互動(dòng)創(chuàng)新實(shí)踐活動(dòng)。透過工程師視角的技術(shù)分享、交流碰撞、項(xiàng)目共建等多種形式,匯聚開發(fā)者的力量,挖掘和傳遞最具價(jià)值的技術(shù)內(nèi)容和項(xiàng)目,全面釋放技術(shù)的創(chuàng)造力。