開源規則引擎——ice:致力於解決靈活繁複的硬編碼問題

聲網Agora發表於2022-04-26

圖片

背景介紹

業務中是否寫了大量的 if-else?是否受夠了這些 if-else 還要經常變動?

業務中是否做了大量抽象,發現新的業務場景還是用不上?

是否各種調研規則引擎,發現不是太重就是接入或維護太麻煩,最後發現還是不如硬編碼?

接下來給大家介紹一款全新的開源規則引擎——ice,以一個簡單的例子,從最底層的編排思想,闡述 ice 與其他規則引擎的不同;講述 ice 是如何使用全新的設計思想,契合解耦和複用的屬性,還你最大的編排自由度。

規則引擎的應用場景

規則引擎在很多業務場景中都有應用,例如:

會員營銷:由多種條件、流程、獎勵組合而成,時間線複雜,程式碼複用率不高,調整頻繁。

風控規則:由多種條件組合並返回決策,條件量大且複雜,變動頻繁。

資料分析:將資料通過分析師自己編排的規則產出想要的資料,千人千面。

以上場景往往都存在一些共同痛點:

靈活業務(變動頻繁,時效性明顯,測試邏輯複雜)

追求靈活花裡胡哨:產品和運營一直在探索新鮮玩法,導致很多抽象出來的模組往往扛不過兩個迭代。

今天上線又要調整:因為一些偶發情況,如線上使用者參與度不高,及時調整使用者參與門檻等(當然也可以在開發前把所有情況考慮到位,但是為了小概率事件做大量的工作,成本過高)。

研發測試心力交瘁:研發硬編碼,測試驗證複雜重複邏輯,久而久之變的愈發疲憊。

時間線(多條時間線交織混亂)

研發編排錯了再來:一般營銷型別的會涉及很多時間線,而在當前,測試一個未來要上線的具有不同時間節點屬性的活動,硬編碼時往往由研發編排時間,測試進行測試,但是當 bug 發生並打亂時間線時,就需要重新編排時間(沒有經歷過的不用太瞭解,後面會說)。

測試並行孔融讓梨:當時間線發生衝突並有多個測試在衝突位置上併發測試,往往由測試自行協調測試順序,當一方出現問題往往導致後續測試進度不可控。

其他問題

依賴掛了難以為繼:測試環境為非穩定環境,一旦依賴出了問題難免影響進度,如何能做到簡單高效 mock?

修復資料苦不堪言:當線上問題產生時,受影響的客戶如何快速高效的補償?

開源規則引擎 ice 的設計思路

為了方便理解,設計思路將伴隨著一個簡單的充值例子展開。

舉例

X 公司將在國慶放假期間,開展一個為期七天的充值小活動,活動內容如下:

活動時間:(10.1-10.7)

活動內容:

充值 100 元 送 5 元餘額 (10.1-10.7)

充值 50 元 送 10 積分 (10.5-10.7)

活動備註: 不疊加送(充值 100 元只能獲得 5 元餘額,不會疊加贈送 10 積分)

簡單拆解一下,想要完成這個活動,我們需要開發如下模組:

圖片

如上圖,當使用者充值成功後,會產生對應充值場景的引數包裹 Pack(類 Activiti/Drools 的 Fact),包裹裡會有充值使用者的 uid,充值金額 cost,充值的時間 requestTime 等資訊。我們可以通過定義的 key,拿到包裹中的值(類似 map.get(key))。

模組怎麼設計無可厚非,重點要講的是後面的怎麼編排實現配置自由,接下來將通過已有的上述節點,講解不同的規則引擎在核心的編排上的優缺點,並比較ice是怎麼做的。

流程圖式實現

類 Activiti、 Flowable 實現:

圖片

流程圖式實現,應該是我們最常想到的編排方式了~ 看起來非常的簡潔易懂,通過特殊的設計,如去掉一些不必要的線,可以把 UI 做的更簡潔一些。但由於有時間屬性,其實時間也是一個規則條件,加上之後就變成了:

圖片

看起來也還好。

執行樹式實現

類 Drool s實現(When X Then Y):

圖片

這個看起來也還好,再加上時間線試試:

圖片

依舊比較簡潔,至少比較流程圖式,我會比較願意修改這個。

計劃永遠趕不上變化

上面兩種方案的優點在於,可以把一些零散的配置結合業務很好的管理了起來,對配置的小修小改,都是信手拈來,但是真實的業務場景,可能還是要錘爆你,有了靈活的變動,一切都不一樣了。

理想

不會變的,放心吧,就這樣,上!

現實

① 充值 100 元改成 80 吧,10 積分變 20 積分吧,時間改成 10.8 號結束吧(微微一笑,畢竟我費了這麼大勁搞規則引擎,終於體現到價值了!)

② 使用者參與積極性不高啊,去掉不疊加送吧,都送(稍加思索,費幾個腦細胞挪一挪還是可以的,怎麼也比改程式碼再上線強吧!)

③ 5 元餘額不能送太多,設定個庫存 100 個吧,對了,庫存不足了充 100 元還是得送 10 積分的哈(卒…早知道還不如硬編碼了)

以上變動其實並非看起來不切實際,畢竟真實線上變動比這離譜的多的是,流程圖式和執行樹式實現的主要缺點在於,牽一髮而動全身,改動一個節點需要瞻前顧後,如果考慮不到位,很容易弄錯,而且這還只是一個簡單的例子,現實的活動內容要比這複雜的多的多,時間線也是很多條,考慮到這,再加上使用學習框架的成本,往往得不償失,到頭來發現還不如硬編碼。

怎麼辦?

讓我們看看 ice 是怎麼做的?

引入關係節點

關係節點為了控制業務流轉。

AND

所有子節點中,有一個返回 false 該節點也將是 false,全部是 true 才是 true,在執行到 false 的地方終止執行,類似於 Java 的 &&。

ANY

所有子節點中,有一個返回 true 該節點也將是 true,全部 false 則 false,在執行到 true 的地方終止執行,類似於 Java 的 ||。

ALL

所有子節點都會執行,有任意一個返回 true 該節點也是 true,沒有 true 有一個節點是 false 則 false,沒有 true 也沒有 false 則返回 none,所有子節點執行完畢終止

NONE

所有子節點都會執行,無論子節點返回什麼,都返回 none。

TRUE

所有子節點都會執行,無論子節點返回什麼,都返回 true,沒有子節點也返回 true(其他沒有子節點返回 none)。

引入葉子節點

葉子節點為真正處理的節點。

Flow

一些條件與規則節點,如例子中的 ScoreFlow。

Result

一些結果性質的節點,如例子中的 AmountResult,PointResult。

None

一些不干預流程的動作,如裝配工作等,如下文會介紹到的 TimeChangeNone。

有了以上節點,我們要怎麼組裝呢?

圖片

如上圖,使用樹形結構(對傳統樹做了映象和旋轉),執行順序還是類似於中序遍歷,從 root 執行,root 是個關係節點,從上到下執行子節點,若使用者充值金額是 70 元,執行流程:

[ScoreFlow-100:false]→[AND:false]→[ScoreFlow-50:true]→[PointResult:true]→[AND:true]→[ANY:true]

這個時候可以看到,之前需要剝離出的時間,已經可以融合到各個節點上了,把時間配置還給節點,如果沒到執行時間,如發放積分的節點 10.5 日之後才生效,那麼在 10.5 之前,可以理解為這個節點不存在。

變化的靈活快速應對

對於 ① 直接修改節點配置就可以。

對於 ② 直接把 root 節點的 ANY 改成 ALL 就可以(疊加送與不疊加送的邏輯在這個節點上,屬於這個節點的邏輯就該由這個節點去解決)。

對於 ③ 由於庫存的不足,相當於沒有給使用者發放,則 AmountResul 返回 false,流程還會繼續向下執行,不用做任何更改。

再加一個棘手的問題,當時間線複雜時,測試工作以及測試併發要怎麼做?

一個 10.1 開始的活動,一定是在 10.1 之前開發上線完畢,比如我在 9.15 要怎麼去測試一個 10.1 開始的活動?在 ice 中,只需要稍微修改一下:

圖片

如圖,引入一個負責更改時間的節點 TimeChangeNone(更改包裹中的requestTime),後面的節點執行都是依賴於包裹中的時間即可,TimeChangeNone 類似於一個改時間的外掛一樣,如果測試並行,那就給多個測試每人在自己負責的業務上加上改時間外掛即可。

ice 的特性

為什麼這麼拆解呢?為什麼這樣就能解決這些變動與問題呢?

其實,就是使用樹形結構解耦,流程圖式和執行樹式實現在改動邏輯的時候,不免需要瞻前顧後,但是 ice 不需要,ice 的業務邏輯都在本節點上,每一個節點都可以代表單一邏輯,比如我改不疊加送變成疊加送這一邏輯就只限制在那個 ANY 節點邏輯上,只要把它改成我想要的邏輯即可,至於子節點有哪些,不用特別在意,節點之間依賴包裹流轉,每個節點執行完的後續流程不需要自己指定。

因為自己執行完後的執行流程不再由自己掌控,就可以做到複用:

圖片

如圖,參與活動這裡用到的 TimeChangeNone,如果現在還有個 H5 頁面需要做呈現,不同的呈現也與時間相關,怎麼辦?只需要在呈現活動這裡使用同一個例項,更改其中一個,另一個也會被更新,避免了到處改時間的問題。

同理,如果線上出了問題,比如 sendAmount 介面掛了,由於是 error 不會反回 false 繼續執行,而是提供了可選策略,比如將 Pack 以及執行到了哪個節點落盤起來,等到介面修復,再繼續丟進 ice 重新跑即可(由於落盤時間是發生問題時間,完全不用擔心活動結束了的修復不生效問題),同樣的,如果是不關鍵的業務如頭像服務掛了,但是依然希望跑起來,只是沒有頭像而已,這樣可以選擇跳過錯誤繼續執行。這裡的落盤等規則不細展開描述。同樣的原理也可以用在 mock 上,只需要在 Pack 中增加需要 mock 的資料,就可以跑起來。

引入前置節點

圖片

上面的邏輯中可以看到有一些 AND 節點緊密繫結的關係,為了檢視與配置簡化,增加了前置(forward)節點概念,當且僅當前置節點執行結果為非 false 時才會執行本節點,語義與 AND 相連的兩個節點一致。

Talk is cheap. Show me the code…

github:https://github.com/zjn-zjn/ice

gitee:https://gitee.com/waitmoon/ice

歡迎大家使用體驗開源的規則/流程引擎 ice。如果有遇到問題,歡迎提 issue 來交流。大家也可以新增作者微信:lwaitmoonl ,備註“ice”,進入交流群。

Dev for Dev專欄介紹

Dev for Dev(Developer for Developer)是聲網Agora 與 RTC 開發者社群共同發起的開發者互動創新實踐活動。透過工程師視角的技術分享、交流碰撞、專案共建等多種形式,匯聚開發者的力量,挖掘和傳遞最具價值的技術內容和專案,全面釋放技術的創造力。

相關文章