需求提出


在业务系统的应用中,常需要定义计算公式。如:工资计算、设备折旧计算、招投标报价等。这些计算方案有一定的规律性,但对于目标客户而言,又是非常个性化,设计并实现一个简单、易用、配置性强的公式计算方案,是业务系统平台的一个重要的基础功能。


目前很多业务系统实现的公式定义很多,功能也非常复杂、基本算术运算、统计公式,参数化定义等,应有尽有。这类公式定义器需要自己也很多的方法与功能,用来实现公式的动态编辑并校验公式的正确性。本文提出的一种公式计算方法,是针对有基础技术技能(懂C#语法)的简单应用解决方法,它直接使用.Net动态编译技术来实现,公式的校验与公式的解析执行非常简单,供大家学习参考。


基本原理


基本原理就是.Net动态即时编译技术。动态编译就是指在运行时动态生成类源代码,执行编译过程,返回编译后的结果类,并执行类的相关操作。目前,动态即时编译技术应用非常多,Java,.Net,JS脚本都支持动态编译。典型的如:JavaScript中有名的 eval方法,直接将参数中的字符串编译为可执行的方法,执行,并返回结果。


使用C#动态编译的主要代码如下:


new  CSharpCodeProvider().CreateCompiler()); 


         //  设置编译参数。      


new  CompilerParameters();  


false;  


true;  


         // 动态创建代码。      


new  StringBuilder();    


         classSrc.Append("public  class  "+  className  +"/n");      


         …… 构造类源码结构     


         //  编译代码。      


         CompilerResults  result  =  complier.CompileAssemblyFromSource(paras,  classSrc.ToString());      


         //  获取编译后的程序集。      


         Assembly  assembly  =  result.CompiledAssembly;      


         //  动态调用方法。      


object  eval  =  assembly.CreateInstance(className);  


         MethodInfo  method  =  eval.GetType().GetMethod(methodName);      


object reobj = method.Invoke(eval,  null);  


return reobj;

对于公式方案而言,要动态构造的内容主要包括公式的参数与公式的计算方法。这些参数与计算方法,都返回一个数值型计算结果,可以简单通过C#中的属性定义来实现。其中,参数类型实现setter与getter属性,计算公式类型实现getter属性。


为了形象说明其原理,下面举一个工资计算的例子进行说明:


类型

名称

说明或公式

参数

性别

男=1 ,女=0

参数

工龄

数值型,年为单位

计算公式

工龄工资

男员工每年50元,女员工每年100元

参数

岗位工资

 

参数

加班天数

数值型

计算公式

加班工资

岗位工资/22.5*加班天数

计算公式

月工资

工龄工资+岗位工资+加班工资

根据上述参数,可以动态构造这样一个工资计算公式类:


///名字空间及引用略     


      public class FormulaClass     


      {     


          参数定义部分     


      private int i性别;     


      public int 性别{get{return i性别;} set { i性别 = value;}}     


      private int i工龄;     


      public int 工龄{get{return i工龄;} set { i工龄= value;}}     


      private int i加班天数;     


      public int 加班天数{get{return i加班天数;} set { i加班天数= value;}}     


      private int i岗位工资;     


      public int 岗位工资{get{return i岗位工资;} set { i岗位工资= value;}}     


      //公式定义部分     


      public int 工龄工资{     


      get{     


return if(性别==1) return 工龄*50;else return 工龄*100 ;


      }     


      }     


      public int 加班工资     


      {     


      get{     


return 岗位工资/22.5*加班天数;


      }     


      }     


      public int 月工资     


      {     


      get{     


return工龄工资+岗位工资+加班工资;


      }     


      }     


      }

 

上述表格中,红色部分是用户根据实际业务需要定义的公式,而公式项目及参数项目都是用户可以动态定义的。


实现过程


要实现公式定义与计算,处理核心的公式计算类生成与编译外,还需要公式项目管理、公式数据存储、公式参数定义、公式配置工具等配套上层应用封装。以实现完整的公式计算解决方案。基本的结构如下图所示:


公式计算类      


       CalFormula
公式方案管理      


       CalSolution
公式参数管理      


       CalParamMgr


公式数据存储



公式配置工具



其中:公式方案管理主要实现对具体的某个公式方案的管理,比如:工资计算方法中,负责对公式动态类的构建、公式的校验、公式的调用及结果返回等。


公式数据存储负责对公式方案及方案中定义的参数、计算方法与计算结果的数据库存储功能。


公式配置工具提供配置界面实现对公式方案的定义。


公式参数管理负责提供公式中需要的数据参数的接口,获取业务系统或用户输入的初始化参数。


由于这些方面不涉及到本文的核心内容,具体功能不详细描述,这里主要对公式计算类一些特殊处理技巧进行说明。


       公式语法校验


公式校验的原理很简单,就是利用C#的编译工具,对动态生成的公式类进行编译,如果出错,表示公式定义错误。但其中的具体问题时,当公式项目很多(比如几十项时),很难通过编译错误信息定位出错的部位。这里的一个技巧是,在维护公式某项目时,将其他所有的项目假设为参数项目,只保留当前一个公式项目,因为参数项目只涉及到getter,setter,不会有计算逻辑错误,这样就很好地对当前编辑的公式项目进行校验。待全部维护完毕无误后,再进行一次统一校验,确保公式定义无误


       公式的循环引用检测


当公式定义中出现循环引用时,会导致运行时堆栈溢出,不但公式计算不能进行,可能还会导致母体程序的崩溃,后果非常严重。而此类问题并不能通过公式语法校验来进行检测,如何处理?这里的一个技巧是,对方案的每一个公式项目的调用次数进行计数,当达到一定的次数(如:200次以上),我们认为出现了循环引用,强制退出,提示公式定义错误。


循环调用的可能情况举例如下:


public int 加班工资     


      {     


      get{      return 月工资/22.5*加班天数;}     


      }     


      public int 月工资     


      {     


      get{      return工龄工资+岗位工资+加班工资;}     


      }

 上面的代码中,加班工资与月工资存在交叉引用,调用时将导致堆栈溢出。


       过程结果的处理


公式方案中,默认只调用一个最终的结果变量,由于存在公式引用关系,与该结果变量相关的其他参数或计算中间结果也将自动计算出来。在构造公式动态类时,每一次调用都可以将当前项目的运算结果存储在哈希表中,供上层应用使用。但由于这个计算过程时有最终结果调用根据公式定义的引用关系触发的。如果要计算的某个过程变量与最终结果无关,不存在引用关系,就不能计算出来。对这个问题,有一个简单的方法处理,就是在最终结果公式定义中,调用该变量,就可以解决。


///此处省略其他代码     


      public int 其他补贴     


      {     


       get{     


       int iVal = 工龄工资/12;     


         resHash.Add(“其他补贴”,iVal);


      }     


      }     


      public int 月工资     


      {     


      get{     


int i=      其他补贴      ;


      int iVal = 工龄工资+岗位工资+加班工资;     


resHash.Add(“月工资,iVal);


      }     


      }

   上述代码中,假设公式的最终结果为“月工资”。加粗黑色的部分为自动生成的用来暂存结果的方法。而红色部分代码,只是对“其他补贴”项目调用一次,目的就是要计算出“其他补贴”项目的值,如果不调用,将不会自动计算出“其他补贴”。对公式的最终结果“月工资”没有产生影响。