前言
背景介紹
規則/流程引擎想必大家並不陌生,耳熟能詳的就有Drools,Esper,Activiti,Flowable等,很多大廠也熱衷於研究自己的規則引擎,都是用於解決靈活場景下的複雜規則與流程問題,想要做到改改配置就可以生成/生效新的規則,脫離硬編碼的苦海。畢竟改改配置和在已有基礎上編排規則/流程,比硬編碼的成本低很多,但是使用市面上現有的規則引擎來編排,一來接入成本和學習成本都不低,二來隨著時間的推移,規則變的越發龐大以及一些場景的不適用,更加讓人叫苦不迭。
「設計思路」
為了方便理解,設計思路將伴隨著一個簡單的充值例子展開。
「舉例」
X公司將在國慶放假期間,開展一個為期七天的充值小活動,活動內容如下:
活動時間:(10.1-10.7)
活動內容:
充值100元 送5元餘額(10.1-10.7)
充值50元 送10積分 (10.5-10.7)
活動備註:不疊加送(充值100元只能獲得5元餘額,不會疊加贈送10積分)
簡單拆解一下,想要完成這個活動,我們需要開發如下模組:
圖中發現有待發放key,這個key是從哪裡來呢:
如圖,當使用者充值成功後,會產生對應充值場景的引數包裹Pack(類Activiti/Drools的Fact),包裹裡會有充值使用者的uid,充值金額spend,充值的時間requestTime等資訊。我們可以通過定義的key,拿到包裹中的值(類似map.get(key))。
模組怎麼設計無可厚非,重點要講的是後面的怎麼編排實現配置自由,接下來將通過已有的上述節點,講解不同的規則引擎在核心的編排上的優缺點,並比較ice是怎麼做的。
「流程圖式實現」
類Activiti、 Flowable實現
流程圖式實現,應該是我們最常想到的編排方式了~ 看起來非常的簡潔易懂,通過特殊的設計,如去掉一些不必要的線,可以把UI做的更簡潔一些。但由於有時間屬性,其實時間也是一個規則條件,加上之後就變成了:
看起來也還好。
「執行樹式實現」
類Drools實現(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元,執行流程:
這個時候可以看到,之前需要剝離出的時間,已經可以融合到各個節點上了,把時間配置還給節點,如果沒到執行時間,如發放積分的節點10.5日之後才生效,那麼在10.5之前,可以理解為這個節點不存在。
「變動與問題的解決」
對於①直接修改節點配置就可以
對於②直接把root節點的ANY改成ALL就可以(疊加送與不疊加送的邏輯在這個節點上,屬於這個節點的邏輯就該由這個節點去解決)
對於③由於庫存的不足,相當於沒有給使用者發放,則AmountResult返回false,流程還會繼續向下執行,不用做任何更改
再加一個棘手的問題,當時間線複雜時,測試工作以及測試併發要怎麼做?一個10.1開始的活動,一定是在10.1之前開發上線完畢,比如我在9.15要怎麼去測試一個10.1開始的活動?在ice中,只需要稍微修改一下:
如圖,引入一個負責更改時間的節點TimeChangeNone(更改包裹中的requestTime),後面的節點執行都是依賴於包裹中的時間即可,TimeChangeNone類似於一個改時間的外掛一樣,如果測試並行,那就給多個測試每人在自己負責的業務上加上改時間外掛即可。
「特性」
為什麼這麼拆解呢?為什麼這樣就能解決這些變動與問題呢?
其實,就是使用樹形結構解耦,流程圖式和執行樹式實現在改動邏輯的時候,不免需要瞻前顧後,但是ice不需要,ice的業務邏輯都在本節點上,每一個節點都可以代表單一邏輯,比如我改不疊加送變成疊加送這一邏輯就只限制在那個ANY節點邏輯上,只要把它改成我想要的邏輯即可,至於子節點有哪些,不用特別在意,節點之間依賴包裹流轉,每個節點執行完的後續流程不需要自己指定。
因為自己執行完後的執行流程不再由自己掌控,就可以做到複用:
如圖,參與活動這裡用到的TimeChangeNone,如果現在還有個H5頁面需要做呈現,不同的呈現也與時間相關,怎麼辦?只需要在呈現活動這裡使用同一個TimeChangeNone例項,更改其中一個,另一個也會被更新,避免了到處改時間的問題。
同理,如果線上出了問題,比如sendAmount介面掛了,由於是error不會反回false繼續執行,而是提供了可選策略,比如將Pack以及執行到了哪個節點落盤起來,等到介面修復,再繼續丟進ice重新跑即可(由於落盤時間是發生問題時間,完全不用擔心活動結束了的修復不生效問題),同樣的,如果是不關鍵的業務如頭像服務掛了,但是依然希望跑起來,只是沒有頭像而已,這樣可以選擇跳過錯誤繼續執行。這裡的落盤等規則不細展開描述。同樣的原理也可以用在mock上,只需要在Pack中增加需要mock的資料,就可以跑起來。
「引入前置節點」
上面的邏輯中可以看到有一些AND節點緊密繫結的關係,為了檢視與配置簡化,增加了前置(forward)節點概念,當且僅當前置節點執行結果為非false時才會執行本節點,語義與AND相連的兩個節點一致。
專業的文件
code
Talk is cheap. Show me the code…
github: https://github.com/zjn-zjn/ice