成本计算引擎动态规则解析技术详解
源寶導讀:隨著企業數字系統應用的越來越深入,業務計算方式也變的越來越復雜,靈活度要求也越來越高。本文將介紹通過將配置動態轉換成可執行代碼的方式,解決業務計算高度靈活化配置的技術方案。
一、背景
? ??ERP本質上是一種“業務密集型”軟件系統,將各種數據經過復雜的業務計算后,以更具業務價值的形式顯示給客戶。在某些特殊的復雜業務場景下,客戶對業務計算邏輯的靈活性有很高要求,這就需要將計算規則做成可配置化,比較通用的做法是,將計算邏輯中的變量抽取成配置項,但不管如何抽取,計算邏輯的代碼都會有”寫死“的部分,如果客戶對計算邏輯的靈活度要求更高時,這種做法將無法滿足需求。比如在ERP中有一個業務功能,需要對”動態成本“數據進行計算,但業務對計算邏輯的靈活度要求非常高,對每一種數據對象的計算規則都可能不一樣,每一個計算表達式都可能會自定義,如果用傳統的可配置化方案,將難以實現這種高度靈活的配置,即使實現出來,也會讓系統和配置非常復雜,造成系統不穩定,用戶難以使用。我們需要一種更優雅的解決方案。
? ? 我們發現,可以將規則配置動態轉換成可執行代碼,通過這種方式既滿足高度靈活的規則配置,又可以大大簡化系統實現,大幅提升系統的穩定性。本文將從”技術原理“和”架構實現“兩個層面,詳細介紹我們的解決方案。
二、技術原理
? ??規則的可配置化的重點在于規則字符串的動態轉換為可執行代碼的環節,最終實現的效果是能夠根據規則字符串的定義,從內存實體對象中獲取指定的字段值并完成計算,再賦值給目標實體對象的指定字段中。
? ? 首先我們看下如何根據定義的字段名稱從內存實體對象中取值。在.Net平臺下,最常用的動態取值方式是反射和方法委托,眾所周知,反射性能是很低下的,因此在數據轉換這樣的大數據量高實時性的場景下,是無法接受的,而委托呢,其實性能和原生取值相差不多而且實現也很簡單,是一個比較好的取值方式,其性能優于反射取值約30-50倍,100W次循環取值僅需耗時數毫秒。取值方式如下:
那么是否委托取值就能解決規則取值的問題呢?顯然還不行,從代碼中我們發現每個字段取值都需要創建一個委托對象,這些委托對象的緩存、提取都需要通過列表對象進行處理,于是代碼可能變成如下:
? ? 這個代碼是極度高效的,但是問題來了,我們期望返回值是通過規則配置的,這樣硬編碼肯定不行,我們希望由配置來決定返回值而不用修改代碼。有辦法嗎?當然有。
? ? 我們先了解一下.Net運行機制,.NET運行時任何有意義的操作都是在堆棧上完成的,而不是直接操作寄存器,而這個堆棧操作則是由中間語言MSIL來執行的,C#、F#等語言在執行前都會編譯為IL語言來執行。而我們前面所寫的這段C#代碼實際上也會編譯成IL語言,所以,要實現不改代碼而又能動態定義返回值,那么我們可以通過Emit構建IL指令,動態的生成該方法。?
? ? 這段代碼用Emit怎么構建IL指令呢?
首先在當前程序域下創建一個新的程序集,并定義一個動態類Builder對象:
然后再為這個動態類構建一個類初始化方法:
至此,一個OrderClass動態類已經構建完成,下一步是構建GetValue方法:
? ? 構建完成后,這個方法仍然是一個空方法,并沒有實現返回值return order.Money。如果要在定義好的方法內實現我們需要的功能,則需要掌握MSIL語法指令,因為篇幅原因,這里不對IL指令展開說明,有興趣的可以查閱相關資料仔細了解。這里我們可以用快捷方法來構建IL指令,就是通過工具直接將C#代碼翻譯為IL指令,然后在代碼中實現。Visual Code的IL View插件提供了直接查看當前C#代碼IL指令的功能,另外也可以將C#代碼編譯為DLL后,通過反編譯軟件查看IL指令。比如以上OrderClass我們可以通過反編譯軟件dnSpy來查看GetValue實現返回值的IL指令:
? ? 有了以上指令,那么我們就能在定義的方法中調用IL指令來實現我們所需的功能,比如我們在GetValue方法中返回Money字段就可以這樣實現:
? ?至此,我們已經完成了這個類的構建,最后我們將這個構造類動態創建為Type類型,以供后面程序使用:
? ??我們已經有了高性能且能動態構建的取值(賦值原理相同)方式,但是離使用規則實現對象間的數據流轉還有一定的距離,因為數據流轉除了取值賦值之外,還有規則運算。一段字符串形式的規則如何能在.Net程序中高效計算并得到結構呢?如果用IL直接構造,顯然過于復雜,但是如果你用過強大Lambda表達式,那么你會發現其正好能支撐我們的規則運算且足夠方便。.Net對Lambda表達式提供了強大的支持,能夠將對象取值運算操作通過Lambda表達式實現并通過Fun委托輸出計算結果,比如我們規則運算就可以用Lambda來實現:
? ? .Net還并且支持通過Lambda表達式動態構造一個方法:
? ? 是不是很方便?和前面IL指令結合在一起,我們就能實現取值、運算、賦值的可配置了。
? ? 當我們在實際應用中去實現數據流轉可配置化時,很快就暴露一個新的問題,規則是通過C#的Lambda表達式實現的,也就是說,這個規則必須在程序中硬寫代碼,無法動態修改規則和存儲規則,更無法通過配置文件來實現。所以我們必須實現將字符串形式的規則轉化為.Net的Lambda表達式對象。
? ? 將字符串轉化為Lambda表達式,需要對Lambda表達式的語法結構有一定的了解。Lambda表達式由表達式樹構成,其結構似二叉樹結構,分左右兩個節點,然后由運算符連接,每個節點的子節點結構也是類似,直到末級節點,每個節點都是一個Expression對象的某個類型的繼承對象,如下圖:
? ? 因此,我們需要將文本的表達式先解析為樹形結構,然后從末級開始,將節點轉化為Expression對象,然后一級級通過運算Expression對象連接起來,最后得到完整的Lambda表達式。
?例如一段文本表達式是“Money + TaxMoney*100/(200+1)+100”,我們需要將其解釋為(Money + ((TaxMoney * 100) / (200 + 1))) + 100,其樹形結構表示為:
? ? 那么通過文本解析出來了以上樹形結構,如何生成Lambda表達式呢?以上的表達式較為復雜,我們舉個稍微簡單的表達式來嘗試轉換,比如從Order對象中取值完成計算Money*100+TaxMoney*50的運算。首先我們將該表達式解析為樹形結構,輸出為(Money*100) +(TaxMoney*50),這是一個典型的二叉樹結構,末級別分別是Money*100和TaxMonet*50,父級為A+B的計算結果。所以我們可以編寫如下代碼來動態構建Lambda表達式:
? ? 我們很容易就完成了將文本表達式動態構建為Lambda表達式來完成計算。在實際的應用中,表示式往往是很復雜的,但是分解到末級,原理都是一樣的,如果能熟練掌握構建方法,復雜的表達式依然可以輕松構建。
? ? 至此,我們實現字符串規則轉換為系統規則的所有技術已經有了,接下來的就是設計一個好的架構,高效、便捷、可擴展的完成規則的可配置化。
三、架構設計與實現
? ? 設計架構之前,我們必須先定義規則的格式,這個規則格式應該能夠滿足我們對業務的有效描述,比如,我們需要將符合條件的A單據的某些字段根據公式計算后寫入B表的某個匯總字段,因此,我們可以這樣描述“當A滿足條件N時,將通過取值公式M得到的值寫入B的L字段”,轉換成規則就是“if(N(A)==true){ B.L=M(A)}”,在成本業務里,會更復雜一些,會有審批狀態P,比如當提交審批(P=P1)時會累加M(A),審批通過(P=P2)時會扣減M(A),于是我們需要將上述規則轉化為“if(N(A)==true){ if(P1) {B.L=M(A)}; if(P2) {B.L=-M(A)};}”。這樣的表訴看起來有些繁瑣,于是我們可以設計一種語法糖來表述這個規則,在.Net下,我們可以用含有Lambda 的格式來表述
“ToSummay<B>(b=>b.L).From<A>(a=>M(a)).Where(a=>N(a)).OnIcrease(P1). OnDecrease(P2)”,如果不用Lambda我們也可用結構化的數據來表述規則,當然也可以根據需求用其它形式來表述。(注:為何要設立OnIcrease、OnDecrease而不只用Where去處理,是因為只用Where的話,不同的審核狀態要寫多個規則,其實值都是一樣,會導致規則量膨脹)
? ? 有了規則模板,我們就可以開始設計計算架構了,先分析上面我們所設計的這條規則“ToSummay<B>(b=>b.L).From<A>(a=>M(a)).Where(a=>N(a)).OnIcrease(P1). OnDecrease(P2)”,可以獲得幾個必要單元:
如果再增加一條規則呢?比如“ToSummay<B>(b=>b.L).From<K>(k=>M(k)).Where(k=>N(k)).OnIcrease(P1). OnDecrease(P2)”,我們可以獲得以下幾個單元:
整理一下就變成了這樣:
? ? 看到上圖,這正是我們要設計的規則解析計算架構中的計算單元對象。我們繼續抽象出接口:
? ? 此時,我們的規則解析計算單元的設計已見雛形,我們可以很容易的將前面的規則表達式轉換成上述接口的實現(字段賦值的方法需要借助IL實現)。但是我們要把它應用到系統中仍然有難度,因為IDocuenmtExp接口提供的都是Lambda表達式,當一個對象傳后,無法直接計算的,必須轉變為可執行方法,所以我們需要對上述設計稍作修改:
? ? 這樣,我們就將文本規則動態轉換為了可執行對象,當我們傳入一個單據對象實體后,計算對象內部就可以完成驗證、取值、計算和賦值操作,這就實現了兩個對象間按照配置的規則完成了數據流轉,而無需編碼。
? ? 下圖是我們成本計算引擎的整體架構圖,可以看到,除了上面介紹的計算核心部件,整個成本計算引擎還包含很多內容,比如:數據合并、數據巡檢、預警強控等一系列功能,保障成本業務數據的高質量。
四、總結
? ? ?本文介紹了ERP成本系統的計算引擎,如何在保障數據質量的情況下,為客戶提供高度靈活的可配置計算規則的技術實踐。這套高度靈活的計算引擎架構,不僅可以應用在成本數據計算的場景,對所有需要高度靈活配置的計算場景都適用。歡迎對計算引擎感興趣的小伙伴聯系我們成本產品團隊,一起交流技術方案和實踐心得。
------ END ------
作者簡介
胡同學:?架構師,目前負責ERP成本應用系統的架構設計與開發工作。
也許您還想看
通過在線編碼提高前端代碼質量的探索與實踐
基于工作單元的高性能實體服務
總結
以上是生活随笔為你收集整理的成本计算引擎动态规则解析技术详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET Core MVC扩展实践
- 下一篇: Kubernetes,多云和低代码数据科