32

成本计算引擎动态规则解析技术详解

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzAwNTMxMzg1MA%3D%3D&%3Bmid=2654077280&%3Bidx=5&%3Bsn=086c461e5f1387c44c8763508af9a25f
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

eeA3umu.png!web

源宝导读: 随着企业数字系统应用的越来越深入,业务计算方式也变的越来越复杂,灵活度要求也越来越高 。本文将介绍通过将配置动态转换成可执行代码的方式,解决业务计算高度灵活化配置的技术方案。

一、背景

ERP本质上是一种“业务密集型”软件系统,将各种数据经过复杂的业务计算后,以更具业务价值的形式显示给客户。在某些特殊的复杂业务场景下,客户对业务计算逻辑的灵活性有很高要求,这就需要将计算规则做成可配置化,比较通用的做法是,将计算逻辑中的变量抽取成配置项,但不管如何抽取,计算逻辑的代码都会有”写死“的部分,如果客户对计算逻辑的灵活度要求更高时,这种做法将无法满足需求。比如在ERP中有一个业务功能,需要对”动态成本“数据进行计算,但业务对计算逻辑的灵活度要求非常高,对每一种数据对象的计算规则都可能不一样,每一个计算表达式都可能会自定义,如果用传统的可配置化方案,将难以实现这种高度灵活的配置,即使实现出来,也会让系统和配置非常复杂,造成系统不稳定,用户难以使用。我们需要一种更优雅的解决方案。

我们发现,可以将规则配置动态转换成可执行代码,通过这种方式既满足高度灵活的规则配置,又可以大大简化系统实现,大幅提升系统的稳定性。本文将从”技术原理“和”架构实现“两个层面,详细介绍我们的解决方案。

二、技术原理

规则的可配置化的重点在于规则字符串的动态转换为可执行代码的环节,最终实现的效果是能够根据规则字符串的定义,从内存实体对象中获取指定的字段值并完成计算,再赋值给目标实体对象的指定字段中。

首先我们看下如何根据定义的字段名称从内存实体对象中取值。在.Net平台下,最常用的动态取值方式是反射和方法委托,众所周知,反射性能是很低下的,因此在数据转换这样的大数据量高实时性的场景下,是无法接受的,而委托呢,其实性能和原生取值相差不多而且实现也很简单,是一个比较好的取值方式,其性能优于反射取值约30-50倍,100W次循环取值仅需耗时数毫秒。取值方式如下:

Vf2euyU.jpg!web

那么是否委托取值就能解决规则取值的问题呢?显然还不行,从代码中我们发现每个字段取值都需要创建一个委托对象,这些委托对象的缓存、提取都需要通过列表对象进行处理,于是代码可能变成如下:

QRNRfye.jpg!web

u6jINvb.jpg!web

这个代码是极度高效的,但是问题来了,我们期望返回值是通过规则配置的,这样硬编码肯定不行,我们希望由配置来决定返回值而不用修改代码。有办法吗?当然有。

我们先了解一下.Net运行机制,.NET运行时任何有意义的操作都是在堆栈上完成的,而不是直接操作寄存器,而这个堆栈操作则是由中间语言MSIL来执行的,C#、F#等语言在执行前都会编译为IL语言来执行。而我们前面所写的这段C#代码实际上也会编译成IL语言,所以,要实现不改代码而又能动态定义返回值,那么我们可以通过Emit构建IL指令,动态的生成该方法。 

这段代码用Emit怎么构建IL指令呢?

首先在当前程序域下创建一个新的程序集,并定义一个动态类Builder对象:

JBFvAb7.jpg!web

然后再为这个动态类构建一个类初始化方法:

nayUjyB.jpg!web

至此,一个OrderClass动态类已经构建完成,下一步是构建GetValue方法:

6Zzmmy2.jpg!web

构建完成后,这个方法仍然是一个空方法,并没有实现返回值return order.Money。如果要在定义好的方法内实现我们需要的功能,则需要掌握MSIL语法指令,因为篇幅原因,这里不对IL指令展开说明,有兴趣的可以查阅相关资料仔细了解。这里我们可以用快捷方法来构建IL指令,就是通过工具直接将C#代码翻译为IL指令,然后在代码中实现。Visual Code的IL View插件提供了直接查看当前C#代码IL指令的功能,另外也可以将C#代码编译为DLL后,通过反编译软件查看IL指令。比如以上OrderClass我们可以通过反编译软件dnSpy来查看GetValue实现返回值的IL指令:

qaMJzmU.jpg!web

有了以上指令,那么我们就能在定义的方法中调用IL指令来实现我们所需的功能,比如我们在GetValue方法中返回Money字段就可以这样实现:

3qyQre3.jpg!web

至此,我们已经完成了这个类的构建,最后我们将这个构造类动态创建为Type类型,以供后面程序使用:

Yfyuiej.jpg!web

我们已经有了高性能且能动态构建的取值(赋值原理相同)方式,但是离使用规则实现对象间的数据流转还有一定的距离,因为数据流转除了取值赋值之外,还有规则运算。一段字符串形式的规则如何能在.Net程序中高效计算并得到结构呢?如果用IL直接构造,显然过于复杂,但是如果你用过强大Lambda表达式,那么你会发现其正好能支撑我们的规则运算且足够方便。.Net对Lambda表达式提供了强大的支持,能够将对象取值运算操作通过Lambda表达式实现并通过Fun委托输出计算结果,比如我们规则运算就可以用Lambda来实现:

.Net还并且支持通过Lambda表达式动态构造一个方法:

ZfYbaib.jpg!web

是不是很方便?和前面IL指令结合在一起,我们就能实现取值、运算、赋值的可配置了。

当我们在实际应用中去实现数据流转可配置化时,很快就暴露一个新的问题,规则是通过C#的Lambda表达式实现的,也就是说,这个规则必须在程序中硬写代码,无法动态修改规则和存储规则,更无法通过配置文件来实现。所以我们必须实现将字符串形式的规则转化为.Net的Lambda表达式对象。

将字符串转化为Lambda表达式,需要对Lambda表达式的语法结构有一定的了解。Lambda表达式由表达式树构成,其结构似二叉树结构,分左右两个节点,然后由运算符连接,每个节点的子节点结构也是类似,直到末级节点,每个节点都是一个Expression对象的某个类型的继承对象,如下图:

RfYfQnv.jpg!web

因此,我们需要将文本的表达式先解析为树形结构,然后从末级开始,将节点转化为Expression对象,然后一级级通过运算Expression对象连接起来,最后得到完整的Lambda表达式。

例如一段文本表达式是“Money + TaxMoney*100/(200+1)+100”,我们需要将其解释为(Money + ((TaxMoney * 100) / (200 + 1))) + 100,其树形结构表示为:

NvAji2V.jpg!web

那么通过文本解析出来了以上树形结构,如何生成Lambda表达式呢?以上的表达式较为复杂,我们举个稍微简单的表达式来尝试转换,比如从Order对象中取值完成计算Money*100+TaxMoney*50的运算。首先我们将该表达式解析为树形结构,输出为(Money*100) +(TaxMoney*50),这是一个典型的二叉树结构,末级别分别是Money*100和TaxMonet*50,父级为A+B的计算结果。所以我们可以编写如下代码来动态构建Lambda表达式:

uaMzMru.jpg!web

我们很容易就完成了将文本表达式动态构建为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)”,可以获得几个必要单元:

vAbyYzb.jpg!web

如果再增加一条规则呢?比如“ToSummay<B>(b=>b.L).From<K>(k=>M(k)).Where(k=>N(k)).OnIcrease(P1). OnDecrease(P2)”,我们可以获得以下几个单元:

BrYjqma.jpg!web

整理一下就变成了这样:

7rmiayy.jpg!web

看到上图,这正是我们要设计的规则解析计算架构中的计算单元对象。我们继续抽象出接口:

UfUfmmF.jpg!web

此时,我们的规则解析计算单元的设计已见雏形,我们可以很容易的将前面的规则表达式转换成上述接口的实现(字段赋值的方法需要借助IL实现)。但是我们要把它应用到系统中仍然有难度,因为IDocuenmtExp接口提供的都是Lambda表达式,当一个对象传后,无法直接计算的,必须转变为可执行方法,所以我们需要对上述设计稍作修改:

NBZjeaN.jpg!web

这样,我们就将文本规则动态转换为了可执行对象,当我们传入一个单据对象实体后,计算对象内部就可以完成验证、取值、计算和赋值操作,这就实现了两个对象间按照配置的规则完成了数据流转,而无需编码。

下图是我们成本计算引擎的整体架构图,可以看到,除了上面介绍的计算核心部件,整个成本计算引擎还包含很多内容,比如:数据合并、数据巡检、预警强控等一系列功能,保障成本业务数据的高质量。

BNZNfeB.jpg!web

四、总结

本文介绍了ERP成本系统的计算引擎,如何在保障数据质量的情况下,为客户提供高度灵活的可配置计算规则的技术实践。这套高度灵活的计算引擎架构,不仅可以应用在成本数据计算的场景,对所有需要高度灵活配置的计算场景都适用。欢迎对计算引擎感兴趣的小伙伴联系我们成本产品团队,一起交流技术方案和实践心得。

- ----- END ------

作者简介

胡同学:  架构师,目前负责ERP成本应用系统的架构设计与开发工作。

也许您还想看

通过在线编码提高前端代码质量的探索与实践

基于工作单元的高性能实体服务

b2Q7ryq.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK