欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

统计类考核类业务系统的设计要点

程序员文章站 2022-03-28 09:30:05
...
    最近一直在修改一个统计报送系统,同时思考一个将要开发的绩效考核系统的设计。一方面发现之前开发的统计系统问题很大,而新设计的绩效考核系统与之也几分相似的地方,所以合在一起总结一下我的经验。

    最近补充了【后记】,仔细说明为什么改,如何思考的,略有缀述。见下面华丽的分割线后的部分。
   
    循环处理是最基本的、最有效的优化代码的方式之一。

一、绩效考核系统的特点
    之前我设计开发过至少2个绩效考核系统,这一次可能与之前的稍微有点一同。

    其中一个是某大企业集团的绩效考核,总公司的人事部门给业务部门、业务部门负责人、分公司、分公司的领导考核,而考核有相互打分,有上级给下级打分,有下级给上级打分,有总公司领导给下级打分,有业绩分,各自的权重都可以配置、一项的分数是由许多其它的分数算出来的。
    另一个是某个部门的对下级的考核,包括上下沟通工作计划,定时汇报工作完成情况,工作也由自评、互评,还有其它几个维度组成。一项的分数也会是由其它很多分数计算出来的。

    上面两个看似差别比较大,但有一点是共同的,那就是打分的字段都是固定的。而新设计的绩效考核系统呢,主要的打分项是业务人员自己设计组合出来的,可能这次这样,下次是那样的。而你不可能每进行一个考核,都生成一个考核分的表出来。所以,我要设计的就是考核分只有一张表,而考核的表头是另外的表。考核分表的记录对应不同的表头,所以同一字段在不同的考核中的意义是完全两样的。
    可以说,设计的维度提高了一级,从而带来的难度也更大了。

    这样一个考核分表,自然有除了基本的字段外,考核分将会用没有实际意义的Score1,Score2...这样的字段名来命名,与考核表表头的顺序号一致。但实际上要考虑到一些变化,很可能某个考核分对应的业务数据要放在一起,这样要多出来很多Biz1,Biz2...这样的字段。也有可能某个顺序之后要求变化,这时候要不要把变化传导到最低层,隔离变化就要尽可能在上层适配。差不多这样的考核分表,要有好几十个分数字段以满足最大可能的考核方案设计。

    可以看出,考核由于有大量的字段,这样的设计,最大的特点就是字段命名的顺序化,可以用循环来处理sql,至于上层,可以用反射循环处理属性,至于页面层,也可以用循环处理页面元素的生成。循环将是处理的核心要点。

二、统计报送系统的设计经验
    与上面考核系统的设计最大的共同点也就在这样,笔者之前设计的一个统计系统的表有70多个字段,数据纵轴是按领域分的,每一个领域横轴又按一些粗类别,细类别,更细的类别,类别总数又分成很多,结果就是70多列的报表。

    关于这类报表的设计,发现了之前的人设计的问题,我总结了以下几个要点,按此设计,应该能减少完全不按此方式的40%以上的开发工作量,而且方便后期维护,方便别人理解,而且不会眼看花了。

    1.所有的统计数字字段命名要按数字排序
    虽然与前面的绩效不一定,这样的表字段都有实际的意义,但是用数字就可以利用上循环。在录入统计数字的页面,可以一层循环就生成一行input录入框,或者两层循环就生成一个完整的录入界面,代码非常少。
    当然input的名字也是含数字的,在后台取值时可以循环从request中获取值,很多地方都从很长的清单代码变成了循环。有的人字段就是实际的意义,很多地方都是长段的重复的一行行代码,而且不是每间隔5个就空一行,看的我的眼睛哦。这才是辣眼睛,看有的代码象在看秋香姐,有的代码象在看凤姐的感觉。而越是字段多,越显的精练。

    2.表头的页面代码的嵌套与共享
    一个复杂的统计报表,表头层次很多。之前有安排让一个同事做一个复杂的表格页面,从上午到下午,我一打听还没弄好。本来估计是一个多小时的轻松工作量,我就看看问题在哪,原来是在研究复杂的跨行跨列。怪不得,我自己估计也弄不出来,弄出来也老眼晕花了。立即指出,表格一定要用嵌套的方式来画。本来表格从业务上来说就是分业务层次关系的,就是一种嵌套关系,外层表格只管外层的行列数,里面的单元格里再画里面的表格,不用管里面的表格是几行几列的。听了我的建议,大概半小时不到就做好了。反馈说,已经在里面绕乱晕了,我这方法十分清晰,三、四层表格很简单就搞定了。

    虽然我想到,但是一般简单的表头也就跨一下算了,但嵌套还有一个好处,可以方便的控制显示层次,而且下层的变化不会影响上层的行列数,算不算面向对象的表头设计呢?如果用户要求分大类显示几列,那层次法由于嵌套,就更方便了。

    最近修改一个系统的表头看的我是老眼晕花了,我必须要按我的标准改造一下。而且在几个页面都出现了相同的表头,那任何程序员都应该努力去消除重复。有时候并不需要多么高超的抽象思维能力,写出抽象核心什么的。感觉简简单单的重复,就可以去除,比如用include的方式(在include里可以判断页面来源,显示input,或者显示数据)。我已经按此方式在处理了,原设计几个页面相同的表头,我改了这里改那里...边改边叹气,这些在我眼里都是无用功,浪费时间的工作,可是又如何在源头阻止这种事情的发生呢?

    前面提到跨行跨列的问题,最近修改的这个更复杂,因为表头太长而折成两行,原作者下下又按用户提供的样子做了跨行,所以更加的乱,更加的复杂...

   3.如何应变
    用循环在最规范的情况下非常好,偶尔也会有用户提出的特殊要求,比如多显示一列什么的,或者交换一下列的顺序之类的。
    前面提到,把变化尽量前置,而不要影响到后面。我们看到,软件中,一般数据库很少变化,而前台的显示中以变化多端。能在前台隔离的业务变化,就不要传递到后台来。

    所以,如果有列的顺序变化,前台循环就分成两段好了,后端不变是原则。如果要在列旁边显示业务数据,那也是一样。

    4.统计字段的数据库字段类型
    原则上,是什么类型我就用对应的类型,比如统计数字,我肯定用integer,金额我用两位的number,绝对不会用nvarchar2来乱存放。一是从层次库到业务bean我都有工具自动生成对象的,而且不适当的类型虽然可以转化,但效率肯定差。
    最近的一个统计库,无论统计数还是金额都是number还没有参数,结果我的工具生成bean都是Interger,还要我修改脚手架代码。而且没有配置PK,自动的BEAN也没ID,都要修改。只能完全按规范做的设计,才最方便的利用工具,利用循环,最大程序减轻低级工作耗费的时间。

    5.统计数据的获取
    当用业务数据生成统计数据时,原则上,我都是用一条sql语句搞定的,绝不会用多个sql返回复杂的数据对象,比如返回map套map的数据。
    上面提到的那个50多个字段的统计表,我写的代码最后生成的SQL放在WORD中统计字符数超过了1000个字符,但逻辑还是很清楚的,SQL中也用到了循环来生成,因为字段名字是有数字排序的。
    最常用的sql中的关键字是 case when then 1 else 0, decode,一个超长的sql语句,数据库也会给你优化,如果有问题也可以在数据库上分析出执行问题,也减少了多次连接数据库的开销,当然用连接池的方式连接开销不大。但一个sql就出来结果,前台是多么的好处理啊。两层循环就搞定了。
    代码非常之少,设计系统时多用脑,特别是任何可能有重复的地方都要多想想,可以把体力劳动转成高效的脑力劳动,即有成就感,也不会把凤姐甩给别人看。


三、总结
    主要就想到以上几点,反射时用Beanutil,PropertyUtil,不要自己写。多个统计算总计的时候,总计对象中的值也是循环各行数据对象,按类型反射赋值。

    后面是实际开发中可能还有一些小的经验,回头再补充。











-----------------------------------------------------------------------
  记得有本书名叫《Head First...》,所以补充整个思考过程
-----------------------------------------------------------------------





四、后记(本项目完整的设计分析)
    本文章在写的过程中,不断有需求变化,而且对原代码改动比较大,下面从头分析下各部分的修改情况:
    1.表设计
    [Head First]:对于一个有70多列的统计报表,原来的设计就是表单一行行硬写,而且也没有5行一空格。保存也是一行行硬写。首先想到的就是用循环来处理,那就需要字段从命名上统一。

    统计报表表头是由多种分类组合而成的,除了多数是次数外,还有几个金额,通常我的想法是把这些列定义成D1、D2、D3...这样的名称,目的是用到反射与循环。其它的列包括PK,数据填报单位,单位ID,填报人,填报时间,上报标志位等字段。
    原来的设计是字段就是英文单词的真实意思,但有很多简单的缩写,让人困惑,但我没有修改字段名。随着用户要求对统计报表逐数据项类别的查询与比对的要求,现在表头已经用树型字典表来配置数据项了,每个系统都会有一个数据字典,而且字典表本身都会进行相应的缓存。自然就选择这个表存表头信息,不需要再建专门的表来记录表头了。同时每个项对应着数据表中的字段名。这样的表头越来越通用化,配置化了。这样通过字典表表头中的循环可以生成表头页面了,而且对于新增或者删除表头项,可以灵活的在字典中修改,而且可以排序。对于原来想的字段D1那样的命名,如果有变更,只能中断完整的循环了,变成几个小的局部循环了,但少用了字典表。所以这有一个取舍的过程,简单了也许应变不足,但也许够用了,根据经验吧。
    不过表头定制实际要求比较多,否则一个递归就生成了。对于字段的类型,原来的设计中不管次数还是金额都用的number,没有参数。用自己的脚手架生成的java都是Integer了,而且在页面校验数据时,我很难分开校验次数与金额。于是修改了几个金额的字段,后缀加上DB,表示Double,这样脚手架也改了点,页面上的input校验时,根据input的名字可以区分了。input的名字是对应java类的属性,为了循环处理request的。

    表头与数据表分离,数据表用数字组合命名还有一个好处,就是数据可以从属于不同的表头,或者是不同的版本的表头。所以数据表字段的意义本身就抹杀了,没必要用特殊意义的名字。不是更通用吗?
    [引申]突然想到,看过一个自定义的办文系统,既然可以自定义字段,那数据列就没有明确的意义,也许也是通过另外一个表的映射才知道具体数据是什么。还有一种设计是数据表是clob或者blob,存放自描述的xml数据,但没有办法方便的查询。
    总结:数据库字段应该是什么类型就什么类型,一定要区别,如果字段名字隐含字段类型就更方便了。

    2.页面上的表头处理
    [Head First]:原有好几个页面需要显示表头,有录入页面,有综合显示。那不就是一种重复吗?我是不是应该考虑把表头都放在公共的inc页面中呢?

    事实上我用了一个inc,不同的页面传递一个参数过来,有add,有view两种。而且由于view页面可能显示一个部门的多条类型数据,也可能显示按部门的统计与合计数据,所以view页面统一接受一个Map(数据行的名称,数据对象),这样进一步实现了公共页面的通用性处理。

    3.列表页面的处理
    [Head First]:原有列表是一个分组数据的列表,原开发人员把基本的数据拿到前台来进行复杂的合并运算。为何不直接用分组SQL,把运算交给数据库来做呢?分组不就是数据库SQL的强项吗?

    一个部门半年一报,有三类数据。而列表要求并不是按数据条数列出,而是报一次算一条数据,这样的列表等于是分组分页的数据列表。
    如何处理这样的要求,原设计人的做法是把一个部门的全取出来,再循环拼接出来,页面上用到一个老旧的“前台分页”的处理方式。而我的目标是用一个SQL从数据库中直接获取正确的分组分页数据。比如要group by 单位名称、年度。但列表中还要有审核人,这实际上是一组内数据,显示在select中的必须要用一个函数来处理一下,常用sum count之类的。开始从groupby分组函数中查,查了oralce没有random,干脆就用max(checkerName)来处理了。考虑到其它地方可能会用这样的查询,于是设计了一个底层方法,参数是分组SQL与总数SQL。此外,为了把分组后的数据与数据对象映射一下,可以返回数据对象,分组的select后的别名都是数据对象的属性对应的字段名。在数据库返回map后,按类的属性循环,再找到属性的annotation字段名,从map中取值,有值的话,再通过propetyUtil反射赋值,得到一组数据对象的一个数据对象。其中还把这个组中的数据个数用了一个其它同类型字段的名。这样由一个数据对象反映一组数据对象的值了。

    4.录入页面的一些前台校验
    [Head First]:之前的开发人员,都是一个个写死的,有多少检验都一个个按名字取值来做,代码很长很长..可以我冥冥之中感觉有重复在里面,那就一定要消灭这种重复,校验的数据项长度不定,我只知道java有(...)参数,自然会想到是不是js也有呢?

    但由于目前前台统计数据录入都是数字排序,可以用统一的一个js函数实现校验或者算小计。利用了arguments的不定项参数。之前我也完全不知,因为java有...参数,想到js要有就好了,一查果然有。
	function dataBalance(){
		if(arguments.length<3) alert('参数数量不正确!至少大于2');
		var totalValue=document.getElementById("Col"+arguments[0]).value;
		var sumValue=0;     
		for(var i=1;i<arguments.length;i++)
		{
		  sumValue+=parseInt(document.getElementById("Col"+arguments[i]).value);
		}
		return sumValue==totalValue;
	}

  使用时可以:if(!dataBalance(1,2,3,4,5));//1是否与后面几项合相等。如果再改,可以前几项与后几项计算是不是平衡,更通用。也可以改改,用来设置小计值。

    5。后台数据的各种形式的统计
    [Head First]:之前的开发人员,对于各部门的合计,居然又建了一个表,结构一毛一样,基础数据表有上报,再触发触发器,再把新数据合并到新表的记录中去。这难道不用group by 中的sum来弄?我要晕过去了

    通常统计报表最后一行是上面所有数据的一个合计,后台计算每一个统计数据的总计,开始一个简单的合计,只算一个部门的2、3条数据,我放在java对象层处理,自然用...参数。但后来我抛弃了下面的方法,因为还有很多统计,也是很多条数据及合计,所以要写个通用的方法。这个也通用,不过太多的反射,应用服务器开销太大了,为什么不利用统计sql由数据库来做呢?
    4.1下面是老方法的合计一组数据对象值,返回一个数据对象值。
	 public static BehaviorStatistic sumBehavior(BehaviorStatistic...list) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException{
		 BehaviorStatistic retBeh=new BehaviorStatistic();
	        Field[] fields = BehaviorStatistic.class.getDeclaredFields();
	        for(int i=0; i<fields.length; i++){
	            Field f = fields[i];
	            f.setAccessible(true);
	            if(f.getType().getName().indexOf("Integer")>=0 ){
	            	int totalInt=0;
		            for(int j=0;j<list.length;j++)
		            {
		            	if (list[j]!=null) totalInt+=Integer.parseInt(  PropertyUtils.getSimpleProperty(list[j], f.getName()).toString()  );
		            }
		            PropertyUtils.setSimpleProperty(retBeh, f.getName(), totalInt);
	            }
	            else if( f.getType().getName().indexOf("Double")>=0)
	            {
		            double totalDb=0;
	            	for(int j=0;j<list.length;j++)
		            {
	            		if (list[j]!=null) totalDb+= Double.parseDouble( PropertyUtils.getSimpleProperty(list[j], f.getName()).toString()  );
		            }
	            	PropertyUtils.setSimpleProperty(retBeh, f.getName(), totalDb);
	            }
	        } 

		 return retBeh;
	 }


    4.2新方法,运用数据库的计算能力,速度更快。
    因为有的统计是上面一条条数据本身就是一组数据的合计,最后一行又是上面合计的合计,计算量太大了,所以改成这样。特点就是上面的一行行数据也是一个个统计sql得到的数据的合,后面的总的合计并不需要把上面的所有数据合并一遍,而是用另一个统计sql得到上面的所有的数据的和。所以一条条小合计与最后的大合计本质上是一样的,都是一组数据求和。于是我只写了一上公共的方法。只传入后面的where条件,select我用class反射来生成,而且抛弃非数据和金额的字段。而这些字段上都是sum函数。
.createSQLQuery(sql).setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);
...
Column col = fs[i].getAnnotation(Column.class);
...

    查询结果还是根据class的属性与annotation的字段名,转换成一个合计的数据对象。这个对象只有int与double有值。

    5.页面中的input,也用循环来弄,但id与name值是完全两样的。id是用数字用于前台校验方便,name用属性,统一用request置对象值保存数据库。
    [Head First]:取名字非常重要,隐含一些规律,让代码更灵活,简洁!
<input type="text" id="Col<%=seqenceNum %>" name="<%=DbHelper.transColumnName2Property(dictionary.getStName()) %>"/>

    实际中有一个其它的数字字段是另外由其它一组名字值页面来算出来的,所以我又在字段名称上隐含了一个潜规则.凡是etc结尾的input,多生成一个hidden的input,来记录名字值页面的详细情况,还可以弹出详细情况页面层。而ect只显示值的合计。合计是int,而hidden的String,是名字值用分割符号组合的。


    6.如何对一个对象一些属性自定义计算公式求值。
    [Head First]:这也是最近一直思考的问题,如何让用户定义公式?定义的公式肯定是一个字符串,思考3个途径:数据库SQL,JAVA层计算,JS层计算。或者构建一个复杂的计算树?这样越搞越复杂了。网上搜索出一个方法还不错
    SQL貌似可以,直接把字段放在运算符里拼接成SQL,实现了字符串变成运算。但看看有没有其它方式。java是无法直接解决,但是javaScript中有一个eval函数是可以执行的,所以,可以通过其他途径执行javaScript就可以做到,而ScriptEngine是java的一个javaScript实现类,所以就找到了方法
    import javax.script.ScriptEngine;  
    import javax.script.ScriptEngineManager;  
    import javax.script.ScriptException;
    ScriptEngineManager factory = new ScriptEngineManager();  
    ScriptEngine engine = factory.getEngineByName("JavaScript");  
    Object o = engine.eval(option);  //option是一个字符串的计算公式。
    double d = Double.parseDouble(o.toString());


    计算公式中可以是对象的属性,通过反射机制最终得到一个字符串(数字与运算符号)的计算公式。剩下的就好办了!



    原页面是全部写死的代码,我们的很多项目代码都很糟糕。现在基本上灵活了,有点向产品化靠拢了。多数工作通过配置就可以应变了,但维护的人要求也高,需要整体理解结构,看出一些隐含的映射规则。有的时候按复杂灵活的配置化方式做,先有了原型系统,可能比先弄个简单的花费多一点时间;但后期需求如果变化了,复杂配置的原型系统体现出优势了,而且只是第一次开发复杂原型系统时间多,后期量产项目会摊薄成本。
    而要形成这样的优势,离不开一定级别的领导,在一定范围内统筹才行,各自为战不行。
相关标签: 统计 表格