Java代码设计风格建议
给项目一定的规范来约束开发的风格,长久来说有利于将整个项目的维护难度由指数难度降低到线性难度的。就像香农使用0和1的二元论来约束信息世界,却可以通过加法模拟乘法,以极其简单的底层逻辑配合单纯重复的工作,以此等效完成复杂的工作一般。对开发规范本身而言,在保障程序维护性的前提下,应是对开发者的约束越少越好。下面介绍keep250在使用的JAVA代码开发风格:
1. 背景
我们设定讨论的场景为一个Web Project,它含有三层架构:
- 负责接收http请求的controller层
- 负责进行业务逻辑实现和三方调用的service层
- 负责从数据库获取数据的dao层
数据的传递通过实体或枚举类型(不通过Map或者List直接传输)
- BO(Business Object):负责service层内部的信息传递,dao层和service层互相传递信息使用
- DO(Data Object):负责service层和dao层主要的数据传递
- VO(View Object):负责service层向三方或controller层传输封装好的实体信息
- DTO(Data Transfer Object):负责将三方或controller接收的数据向service传递
graph RL; dao层-->do; do-->dao层; service层-->do; do-->service层; dao层-->bo; bo-->service层; service层-->bo; service层-->vo; 三方-->dto; vo-->三方; dto-->service层; controller层-->dto; vo-->controller层;
2. 对象类
8000行代码的service和1000行代码的service,大概率是后者更容易阅读,使用对象类就能辅助把8000行代码压缩到1000行。压缩的主要方式就是将于对象类相关的方法直接放在对象类中,而不是四散在service中。让对象类作为一个独立的个体,能够自己处理自己的事情,保护自己和发展自己。
对象类包括:DTO、VO、BO、DO。下面简单介绍这几个类的使用规范:
2.1 使用DO类作为从数据库提取数据的承接者(Data Object)
从数据库提取的数据可以用String,或map来承载,但是后期太难维护,不熟悉业务的人很难理解容器承载的字段是什么含义,就不得不到dao层去读sql。
所以,对于从一张表提取数据的情况,建议建一个DO对象,与表字段一一对应,从表中提取的数据装入do类。从多张表提取数据的情况,建议新建一个bo类来承载提取的字段。相较于使用map,DO和BO对象可以提供数据自检能力。在DO类中可以提供方法让DO对象自己检查提取的数据是否符合业务要求,自己判断获取的数据组合属于的种类。
同时,因为DO类是从数据库的持久化数据提中提取出来的数据,所以一般不建议在DO类中提供set方法,而是只提供get方法和各种数据组装和数据性质判断的方法。让其作为一个有自我检测能力和判断能力的对象存在,却不会随便插手数据库的事情,只做一个忠实的数据传输者。只有在无法通过框架反射赋值时,再提供set或构造方法。
2.2 使用DTO和VO类进行项目外部的数据传输者(Data Transfer Object,Value Object)
数据传输一个很方便的做法是通过map做数据的承载。这样做的好处是可以随便拓展和修改外部系统传输和接受的数据,坏处就是太自由了,不看传输者的代码无法知道map里面传输了什么,而传输者一般是前端或三方,会增加维护成本。而且对传输数据的处理都要写在service层,导致service层原本的业务处理代码被数据封装代码冲乱,很难看懂。
不用map的一个替代方案就是用DTO和VO这样的传输类进行数据的传输。一般我们定义DTO为从项目外接收数据的对象,定义VO为从项目往外传送数据的对象。在DTO和VO中,只提供单位字段get的方法、构造方法的方法,一般不对单个字段提供set方法和二次格式化方法。如果实在需要单点更新类中的某个字段再单独提供set方法。为什么对set方法这么防备,是因为我们将VO和DTO这种传输数据的对象类,定义为一个传输数据的承载者,使命是传送数据和解释数据,而不应有能力修改数据。和DO类一样,能交给DTO和VO处理的方法都直接写在对象中,让对象自己有能力处理数据,service层只需要知道它们的处理结果就可以。
特别是DTO类提取出来后,当调用方对数据的格式有变换时,只需要修改DTO的字段就能够应对变化,而不需要修改service层,也不需要影响到DAO层。相对于使用map这种传参方式,DTO和VO需要提前确定所有的字段,这需要和产品、前端做前期调查工作。不过前期的充足准备,能够极大降低将来的维护成本且增加对入参出参的认知和未来的拓展,这代价是值得的。
2.3 使用BO类进行service内部数据传递(Business Object)
BO类的作用是在service层中做业务传输,在service层中起一个数据承载者的作用。对BO类,提供set和get方法,且可以根据业务逻辑需要提供各种方法能力。
此外,因为DO的定义是与数据库的单张表一一映射,所以对于要从多张表提取复合字段时,可以新建一个BO对象来做承载。如无必要勿增实体,除非必要,这个时候的BO就不建议提供set方法了。
3. Controller层
controller层在web的项目中主要负责接收外部的访问请求,这一层应该承担数据校验的责任,确保进来的数据都是合法的数据。后端应该由自己的能力去应对不合法的数据,比如不接受不合法数据,对不合法数据返回提示语等。
在这一层会负责向service层传输DTO,或替service层转出VO。DTO和VO一般通过JSON直接转实体或构造方法入参生成。
这一层的调用主要是service层提供的方法,不该直接调用dao层的方法。
4. Service层
4.1 service设计主思路
思路就是:简化所有可以简化的代码,只保留业务逻辑。
代码应该照顾人看的,然后才是给电脑执行的。service就写最核心的业务逻辑,其他的数据封装、数据校验这些逻辑,不要写在service的方法中。最好一眼就能看到service中某个方法的入参到出参的全部业务逻辑,不掺杂各种set方法、put方法,只是单纯的业务。service层是处理业务的主力层,在service层的方法中,代码应该专注在业务逻辑的实现,对象类的数据封装工作应该交给对象类来做而不是在service层中直接写各种set和get方法去给对象类赋值,这会极大的增大service层代码的阅读理解成本,把重要的业务逻辑淹没在数据组装的代码中。所以建议把数据组装的代码尽可能的融入到对象类中去,让对象类们自己把数据组装好,在service中直接使用。示例如下:
// 计算一个用户的等级是否大于3
public boolean biggerThanThree(UserDTO userDTO) {
// 入参检查
if (userDTO==null || !userDTO.check()) {
return false;
}
// 使用helper工具类化简service方法中的业务逻辑,计算业务
Interger result = CalculteHepler.calculate(userDTO);
// 做回传判断
return result!=null && result>NumEnum.THREE.getValue();
}
如果发现service中不得不进行大量的与业务逻辑无关的逻辑,那么建议把这些逻辑整理成一个个独立的方法,然后在做一个工具类helper,用这个工具类把这些繁杂的逻辑收拾起来统一处理。当service中的业务逻辑遇上这些跳不开的非业务逻辑的时候,就调用helper去解决,然后把结果直接拉回service继续做业务逻辑。helper建议尽量写成static的形式。
service中的业务逻辑有时会有异常抛出,对于抛出的异常可以直接在service层抓掉打日志,也可以抛出上层(一般是controller层)去处理。
此外,一般不要在catch里写业务逻辑,catch应该做catch的事情,业务逻辑就在catch外写。
4.2 查询类service设计建议
- 先从DAO层提取出一条主干数据(把这条数据想象成一艘没有装任何集装箱的集装箱船)
- 然后再从DAO层提取剩余的数据(想象成各个不同的集装箱)
- 组装数据,把全部的数据整合起来(把集装箱装进集装箱船里),成为BO或者VO返回给调用方(把船和集装箱打包送到目的地码头)
5. DAO层
dao层就是从持久源(一般是数据库,比如oralce,mysql)提取数据使用的。这个层建议提取数据尽量提取独立的DO,独立于业务逻辑提取。如果是一些考虑到效能的编码,需要从多张表提取组合数据,可以用一个BO类去接住这样的数据。dao层从数据库提取数据的sql,应该尽可能的简单。越简单的sql就越不容易犯错误,越不容易出现性能问题。
# 下面这个sql,有向复杂sql转化的趋势,且提取的数据一般只能供单个场景使用
# 合并sql
select
a.a1, b.b1, b.b2, b.b3, b.b4
from
table_a a, table_b b
where
a.id = b.id
and a.id = '123';
# 下面对sql的拆解,提取的数据用DO类接受,查询两次其实并不会比查询一次sql性能有巨大差距,但两条sql都能够为以后的业务做复用使用
# 此外,两条sql本身都非常简单,相较于复杂sql,更不容易出现sql问题和性能问题
# 拆分sql1
select
a.a1
from
table_a a
where
a.id = '123';
# 拆分sql2
select
b.b1
from
table_b b
where
b.id = '123';