架构一个项目需要考虑的问题非常的多,诸如性能保证、规范的同时方便开发、服务器的成本考虑、部署方式等等,具体要如何选择框架去完成项目的架构,即使是经过了深思熟虑,也总会有未能考虑到的地方。我在自己思考再三且完成了Demo项目架构的情况下,准备将这个架构的思路尽可能地表达出来,望大牛指教,同行互相讨论学习。
首先,在架构总体思路上,我选择了DDD即领域驱动设计的思想,至于DDD的具体介绍则不在本文范围之内。
1、减少层级
在决定使用DDD思想后,我下一步是要减少层级。
在其他一些项目中,通常会在service层上加上一层business层(或者domain层),这一层甚至可以独立成一个模块(再往外延申就接近中台的概念了,我虽然没有中台的实战经验,但是在思想上我认为是这样实现的),让架构更加灵活。但是我始终认为,这样的做法有点把service层当成基础工具类的感觉,非常难以思考如何让service层发挥更多的作用。所以,在项目中,我的service即是business,于是往常的三层架构中的service在这里被重新定义,它承担了比三层架构更多的责任,且加入更多的规范,使得service更加丰富的同时,成为了领域设计的核心所在。至于具体如何规范service层,我将在后面陈述,因为我想先把持久化层说完。
2、持久化层单表操作
在持久化层,不同的项目叫法也不一样,dao层、mapper层、repository层等等,名称或许与其项目所使用的技术有关。项目中我选择了MyBatis框架以及MyBatis Plus增强框架,并且不建议书写任何的SQL代码。这里涉及两点原因:一、性能考虑,在MyBatis Plus的封装中,所有的操作都是单表操作,不连表则不会在数据库中通过笛卡尔积生成一个巨大的虚拟表(只要有任意一个表的数据量较大,连表生成的虚拟表都非常的大),数据库的性能上能够有一定的保障。将一些连表的操作通过Java代码来实现,对于性能的保障是非常值得的。二、在领域中,每个实体都代表一个领域,实体表示最基本的领域划定,如果书写SQL来做一些连表操作,那么将破坏领域的设计。领域之间的更多复杂关系等,应该在上层(即上述的service层)通过Java代码来体现。就像给地图画上道路、标记等,并非只有房子等建筑属于地图的一部分,错综复杂的连接关系也属于地图,这样的关系在Java中体现才更加明确,且符合DDD的领域思想。另外,选择MyBatis Plus框架的原因是:本来计划只允许单表操作且更着重于上层的代码实现,那么MyBatis Plus框架的封装显得既方便又保持了领域实体的完整性,杜绝任何的SQL代码的存在。
持久化层只有一层,直接由MyBatis Plus封装,于是所有的轮子都有了,接下来就是service层的表演了。
3、service层的方法符合开闭原则
在service层,书写的方法不首先考虑通用性而是考虑专用性,比如,一个保存的方法,不允许直接使用实体来传参,然后调用持久化层的方法来新增或更新——这样的方法逻辑过少,并且因为其通用性,使得调用方的风险和影响可能性极大;其次,这样少逻辑的方法何必存在呢?还不如直接去掉service层,直接在controller层调用持久化层的方法就好了,何必多此一举?
service层的方法不首先考虑通用性的目的就是我希望将service丰富起来,而不是把业务逻辑“巧妙地”推给controller去完成。service中方法应该具体且使用方式明了,也就是需要符合开闭原则——方法职责明确且单一,不允许随意做出会影响调用方的更改。还是用保存方法举例,如果实体的字段不是很多,建议每个字段都用方法形参来传递,且对参数做必要的校验——明确告诉调用方,这里需要这些字段,传参时不要忘记了(如果是一个实体的对象,即使是少了哪个字段,调用方又怎么会知道呢?一个对象很难描述清楚一个方法的实际规则,方法内做了必要校验还勉强说得过去,但是涉及一些例如创建时间的字段时,也让实体对象传过来,如果有误,岂不是出现难以发现的BUG?)
如果实体的字段非常多呢?建议依然使用方法形参来传参或者创建DTO类,前者同上,后者则不少人会觉得多此一举,这样的DTO字段大部分与实体的字段相同。但其实这样的DTO并非多余,它相当于重新描述了领域。打个比方,实体类就是一栋房子,它不仅包含平地以上的部分,也包含了平地之下的部分,而房子之外的比如道路等不会被描述;而重新定义的DTO则通常只会描述平地以上的部分,且把周边的道路描述清楚。
也就是说,一个领域有它狭义的描述,也有它广义的描述,任何的领域都不会是独立存在的。实体类可以理解为当它独立存在时,进行的狭义的描述;DTO则是在领域与其他领域发生交集或者其他逻辑关联时,对领域的广义描述,而service中方法的逻辑会将这些数据处理后回归狭义,回归实体,回归数据库。
也许,单纯的保存方法过于简单,使得上述的一些解释显得有点苍白无力,但如果某个方法设计较为复杂的业务逻辑时,这样的DTO存在的意义才会被放大,从而有利于我们理解它的作用。
4、controller层保持干净
最后,controller层的代码应该尽量只存在一些校验逻辑以及协调service去完成某个逻辑的代码,主要的逻辑都保留在service中。但是要切记不要钻牛角尖进入了另一个误区,就是:把所有的代码都封装进service,使得controller中基本不会有任何其他的代码——在这里我认为可以有另外一条原则:能够在controller中调用service中的方法去协调完成的话就尽量不要在service中调用另外的service;简单点:原则上不在service中调用service;确实有必要则必须先经过深思熟虑,而不是如何方便如何做——规范的目的是为了更好的维护,同时也保持了架构的完整性,不要在实际书写代码的过程中把原来的架构瓦解。
此外,controller中应该必须使用DTO来接收参数,接口要求是JSON字符串。同时,DTO的参数尽量在DTO内部进行校验,需要复杂逻辑才能校验的才调用service中的方法。
至于VO则视情况而定,这里允许存在实体类等,灵活使用即可。
5、区分DTO,且使用Lombok来省略set、get等
service层的DTO是controller跟service通信的协议标准;controller层的DTO则是请求调用方与项目通信的协议标准。他们的DTO不允许使用在其他任何地方,也就是说它只能用来接收参数,而绝不允许为了方便就将controller的DTO来传到service等地方中去,service中的DTO同理,也不允许传到持久化层或controller层等地方去。这些要求都必须明确遵守。
无论是实体类还是DTO、VO等,都使用Lombok工具来省略冗余代码,节省的时间拿去好好写DTO等,不要觉得这时多余的存在,必须遵循规范。
题外话:
MyBatis Plus在版本3之后封装了一些service方法,以供开发者在service中继承后直接在controller中使用,这样的方法在项目中不被允许的原因也是因为违背了上述的规范:不要把controller写成一个大胖子。www.jintianxuesha.com MyBatis Plus在持久化层干活就够了。
小结:
本文阐述了如何活用service且遵循一定的规范去在基于DDD思想的项目架构中书写代码,如此架构足以应对最高单表数据量在千万级的项目,性能维持在秒级(当然也涉及到数据库优化等,如果没有优化也是不行的,本文不讨论),同时能让项目拥有相对较好的代码规范,留给后期运维更多的空间。