最近接手公司服务端接口的相关编写工作,遇到了一些问题,提出了一些想法,讨论了一些问题,与项目经理在方案选择上有了一番争吵(当然,这种争吵是家常便饭的事儿)。特此有了一些心得体会。
方法入参的设计
我们在设计程序的时候,如果使用常规的分层模型,既Controller、Service、Dao,来对项目进行分层,一定会遇到一个问题,就是不同层及之间,数据传递,即每个方法的“入参”应该怎么设计。
我曾待在一家从事银行系统开发的公司,项目有一个很大的特点,它并非使用C、S、D来进行分层开发,当然这不是重点,重点是它虽然使用Java,但是大部分不以面向对象的形式来编写程序,项目中随处可见的是static的静态方法,几乎什么功能都可以用静态方法来完成,我的理解它已然是一个用java以面向过程的思想开发的项目。这个项目在方法之间传递数据时,不适用对象封装,而是采取多个入参的形式,所以很常见的就是要一个方法,有N多个入参,5个是常见、10个都不特殊。这种方式来编写方法,有一定的缺点:
- 在阅读代码的时候,经常会不明白参数的意义,而需要仔细的去看javaDoc上的注释,影响效率;
- 调用一些工具时,因为不是以面向对象的方式去设计程序,所以所有变量的作用域自然只在方法体内,于是对于一些需要设置参数的工具,简单的例如:分页工具,就需要通过一大堆的入参,经常会搞不懂入参的意义,每次调用起来需要重新设置一大堆的参数值;或者使用一个Map来装载这些参数,这样带来的问题就是你甚至不知道有什么参数需要设置,经常会出现错误。
当然,会用这种方式来设计程序,当初项目经理的意思是说:由于变量的作用域仅仅存在于方法体内,当方法执行结束以后,变量就会进入销毁,能快速的回收内存。虽然我并没有真正的去测试过这个观点的正确性,但就我对JVM的GC机制来说,这种做法,是不是会造成大量的垃圾对象,这样GC就会频繁被调用来回收这些对象,这反而是增加了内存的消耗。
有了这次经验,我自然是非常反对为方法设置大量参数作为入参,以对象作为数据载体,进行传递,我认为才是最好的方式。但,依然产生了一些问题。
要用什么对象来传递参数?
由于种种原因(这里就不需要深究为什么了),我们项目在设计接口的时候出现了一个讨论的命题:
要用什么来接收来自前端的请求数据?又如何把前端想要的数据,按照前端需要的数据接口形式,传递出去?
简单介绍一下会有这个问题的背景:
我们在设计接口的时候,往往会遇到这种问题:一个数据接口所请求的数据,分别来自于不同的数据库表,而同时它们也可能在程序中,是分别属于两个不同的对象的成员属性,那这个时候,我们要怎么去设计接口,如何返回数据?
我先来揭晓一下这个讨论结果的最终的决定:
我们项目因为不希望不同层在传递数据是,不知道具体要传递的数据内容,所以不希望我们使用Map来作为载体,而尽可能的使用对象来做。这一点我是没有异议,表示很赞同,因为我曾就深受其害,Map真心不认为是一个适合用于传参数的载体,当然这也不是一棍子打死,只是最好不要使用。但经理让我们这么做,新建一个特殊的对象,这个对象没有任何实体意义,仅仅是为了接口的形式,而去设计的Class,然后把从数据库中查询出来的已经映射成实体对象的数据,拆包,根据对应的成员对象再组装到这个新的对象当中。这句话可能不好理解,我举一个例子:
有一个实体对象(我们叫他Class A),它有10个成员属性,分别是:a、b、c、d.....;但是某个接口仅仅只需要请求这10个成员属性中的某3个属性,于是我就要为了这个接口,专门创建一个类(我们叫他:Class B),这个类只有3个成员属性,没有错,Class B 只不过 是Class A的一个子集。
我们使用的是Mybatis框架,本身查询出来的数据,就已经是一个实体类,具有最全的属性,然后我要把这个实体类的每个接口请求的对应属性都get出来,然后再set到这个新类当中。我把这个get的过程,称作“拆包”,set的过程,称作“装包”。
当然我不认为这是一个好的设计思路。
- 因为这么做首先是每次新增一个接口,就要新建一个class,这种做法本身就违反了程序设计的复用性原则,每个class几乎都是无法复用的。
- 每个接口,每次请求,都需要做一个多余的get、set的动作,这个动作,局部看起来也不是很多操作,但是放到一个高并发的环境中,我认为真的代价很高。
- 从前端接收请求参数时需要创建一个特殊的,只有请求参数的载体对象,数据库查询后需要创建一个映射实体的对象,然后接口返回也要再来一个专门的对象,一个请求最少也需要至少3个对象来完成,一次请求,产生3个对象要等待GC回收。
使用实体类作为数据载体
我们之所以会选择这种方式,也是有原因的,我们是以json形式返回数据,spirngMVC可以添加一个过滤器,把null值的属性都去掉,所以我提出的观点自然是通过实体对象来作为数据承载,一次简单请求由一个实体对象来承载所有的请求数据,并且承载数据库返回的查询数据,这样一次简单请求自然就不会产生什么多余的对象和不必要的操作,然后通过json的not_null过滤器,把null的属性过滤掉,这样接口就干净简洁。
而且我们基本上都是以面向对象的方式来设计程序的,关系型数据库也基本上是根据实体来设计表结构,所以表和实体的映射非常容易设计,关联性很强,每次数据传递接口也不会很多参数,因为毕竟一次请求所需要的数据,跟他发来的请求条件,都会有一定的关系,所以依据对象之间的嵌套关系,实体对象很容易作为数据载体进行方法之间的传递。
但是,这样不代表就完全没有问题了,而这个问题我也不知道如何解决:
一个对象假设有10个成员属性,而一次请求,有1个以上,但可能也就是2个或3个数据参数作为查询条件,那我却要为了这2-3个参数,去实例化一个具有10个成员属性的对象,虽然另外的7-8个成员属性可能没有值,但是不代表他们不消耗内存。
这个问题我一直都想不通,是否有可以解决这类问题的办法?项目重构,将经常调用的参数进行抽象成父类确实能够缓解一点,但也不能完全避免这种内存的浪费。
就算没有值,也要设置一个默认值来表示
但前端那边提出了这样一个问题,有的请求,某些字段确实会有出现没有值的情况,即null的情况,那就会被过滤掉,结果键值都没有,那我就不知道到底是后端没有数据,还是程序出错。于是提出,就算是null,也不能把键值去掉。
其实当时我被这个问题问蒙了,也没有什么好的解决办法。但是回头想一想。。。如果后端返回null,你前端就一定能够确定,不是程序问题么?如果程序出问题,一样会出现null的返回结果,数据设置失败了还不也一样会值为null。要分清楚这个问题,就要能够确定这个值是否是“人造”的数据。
所以我认为,不管是何种数据类型,都应该尽量(同样不能一棍子打死,有的数据确实没办法设置这种默认值)去设置一个默认值来作为这个数据没有值的一个表示,如时间可以设置为00:00:00,正整数可以设置为-1。还有一些字段必填的自然要做好必填项的校验。