title: 辅助审查系统的代码书写规范
date: 2019/10/26 09:43
remark: 本系统采用SpringBoot + Dubbo进行开发
前言
近期在重构辅助审查系统,发现随着项目的发展,代码变得异常混乱,完全违背了当初定下来的规范,当然这其实是无法避免的,毕竟时间紧任务重,哪里有时间让你深思熟虑的考虑这段代码该怎样写。
之前的代码规范只在自己的心中,随着时间就会慢慢遗忘,在写新的代码的时候,可能就想不到那么多,从而导致代码的“味道”越来越差,由此我决定花一天时间,将其落实到纸上,日后写代码的时候可以看一下,尽量保证代码的味道不要太差。
当然,由于项目时间紧任务重,可能没办法所有的代码都按照规范来写,有的时候只能寻找一些捷径,这样也是不可避免的;希望大家日后有时间的时候,可以按照本规范,将违背了规范的地方进行重构。
注:由于辅助审查系统的特殊性,本规范可能不适用于其它系统。
一、工程架构模型
1.1 如何分层
本系统采用的分层结构与p3c规范中的基本相同。
Dao层
数据访问对象(Data Access Object),用于对数据库进行访问,负责数据的CURD。
当然 Dao 不仅限于与数据库进行交互,假如日后系统引入的ElasticSearch、Mongo 甚至Redis,我认为都可以定义一个Dao对其进行访问。
这样的定义,可以将数据的CURD和业务逻辑进行分离。
Manager层
p3c规范中对其定义如下:
- 对第三方平台封装的层,预处理返回结果及转化异常信息
- 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理
- 与 DAO 层交互,对多个 DAO 的组合复用
由这个定义并结合我们的系统,可以得出Manager层主要作用为:
- 可以通过 Manager 层调用第三方服务(指标系统、全文检索服务,因为他们是基础服务,所以不要在 web 层调用他们),对返回结果进行简单的处理之后返回 Service 层。
- 如果需要对基础服务进行调用,并将其结果处理后入库(例如:档案管理系统、模型系统)这种用法时,直接通过 Dao 层进行保存。
- 可以将 Manager 理解为对通用逻辑的封装,避免 Service 与 Service 进行相互调用,以及对通用逻辑的管理。
在开发中,我们经常会遇到 AService 中的某个业务可以提供给 BService 调用,从而让 BService 调用 AService 的方法,认为是 Service 之间具有共同的业务。其实 Service 之间没有共同的业务,而是具备通用的逻辑,这时应该将其抽离出来放在 Manager 中。无论何种工程架构都好,我都不赞同 Service 与 Service 之间的相互调用。
- 如果2个(或2个以上)表之间有一定的关联关系(一对多、多对多)并经常一起使用,则通过一个 Manager 对他们进行访问。
- 可以在这层加入缓存(当然我们体量小,一般只在service层加缓存),与我上面所说的为操作Redis定义对Dao进行访问。
Service层
对具体对业务逻辑进行操作(复用性很低),由于我们使用的是dubbo,Service层可能会被其他人调用,所以最好还是做一下参数校验(hibernate validate)
从p3c规范来看,如果不用Manager层来对多个Dao进行组合,那么Service层可以直接与Dao进行交互,但是这样会给人一种很混乱对感觉,所以我们定义:
Service对数据库进行操作时,必须通过Manager层,其实这也是为日后开发可能遇到对问题留有余地;如果我们使用Redis做Service层缓存的话,那么可以直接调用对应Redis的Dao。
Web层
web层只做简单对参数校验,或者简单对业务处理等(例如:查询审查任务实体,但是前端只需要部分字段,进行转换的工作);Web层与Service层一一对应。
1.2 分层领域对象
本系统采用对模型为贫血领域模型,p3c规范定义对数据传输对象过多,这样就导致了一个对象可能会出现3次甚至4次转换在一次请求中,当返回的时候同样也会出现3-4次转换,这样有可能一次完整的“请求-返回”会出现很多次对象转换。如果在开发中真的按照这么来,恐怕就别写其他的了,一天就光写这个重复无用的逻辑算了吧,所以我们只定义了几个对象。
贫血领域模型中对象只作为数据载体,只有 getter/setter 方法,而不包含业务方法。
DO(Data Object)
数据对象,对数据源数据的映射,如数据库表,ElasticSearch 索引的数据结构。所在包一般命名为 data 。
DTO(Data Transfer Object)
数据传输对象,业务层向外传输的对象。如果在某个业务中需要多次查询获取不同的数据对象,最后将会把这多个数据对象组合成一个 DTO 并对外传输。所在包命名为 dto 。
VO(View Object)
显示层对象,通常是 Web 向模板渲染引擎层传输的对象。现在的项目多数为前后端分离,后端只需要返回 JSON ,那么可以理解为 JSON 即是需要渲染成的“模板”。我一般会将这类对象命名为 xxxResponse ,所在包命名为 response。
Query
数据查询对象,数据查询对象,各层接收上层的查询请求。
其实一般用于 Controller 接受传过来的参数,可以将其都命名为 xxxQuery,而我个人习惯将放在 request body 的参数(即 @RequestBody)包装为 xxxRequest ,而如果使用表单传输过来的参数(即 @RequestParam)包装为 xxxForm ,并分别放在包 request 和包 form 下。
注:web层向service层传输对query对象,绝对不能传输到Manager层,因为Manager层是通用的逻辑。
层间对象传递
其中DTO如果不可复用,那么可以直接传输给前端。
1.3 包结构及其含义
辅助审查服务模块包设计
x5456deMBP:dgp-dubbo-server-root x5456$ tree dgp-ars-server-service/src -d -L 8
dgp-ars-server-service/src
├── main
│ ├── java
│ │ └── com
│ │ └── dist
│ │ └── ars
│ │ ├── aop
│ │ ├── config
│ │ ├── dao
│ │ ├── manager
│ │ │ └── remote
│ │ │ ├── ams
│ │ │ ├── ims
│ │ │ ├── mms
│ │ │ ├── pms
│ │ │ └── sms
│ │ └── service
│ └── resources
│ ├── META-INF
│ │ ├── dubbo
│ │ └── services
│ ├── config
│ ├── db
│ │ ├── oracle
│ │ │ ├── create
│ │ │ └── update
│ │ └── pg
│ │ └── create
│ └── libs
└── test
└── java
└── com
└── dist
└── ars
├── service
└── manager
辅助审查Api模块包设计
tree dgp-ars-server-api/src -d -L 8
dgp-ars-server-api/src
└── main
└── java
└── com
└── dist
└── ars
├── constants
├── exceptions
├── helper # web层与service共用的辅助类
├── model
│ ├── dto
│ ├── entity
│ ├── query
│ │ ├── request
│ │ ├── form
│ │ ├── webQuery # 由web层封装向service层传输的查询对象
│ │ └── commonQuery # 由service层封装传输到Manager层的通用查询对象
│ └── vo
└── service
helper包
开发中会遇到一些很基础的,通用的业务逻辑,例如我们可能会根据每个用户的信息生成一个唯一的 account id 。又或者说有一个用户排名的需求,我们将从用户的相关信息中计算出一个分数,从而根据这个分数进行排名。那么这时候我们可能会将这些逻辑写在 User 数据对象或是其他相应对应的数据对象下。
由于我们采用的是贫血领域模型,数据对象中不应该包含业务逻辑,所以我们将这些通用的业务逻辑都抽出来,放到 helper 包中进行统一管理。如会将生成 account id 的逻辑放在 AccountIdGenerator 中,将计算排名分数的逻辑放在 RankCalculator 中。
我将这些类都归为 Helper ,用于提供底层的业务计算逻辑。而为什么不放在通用工具层中呢?因为这些 Helper 其实都是依赖于特定的领域,即特定的业务。而通用工具类则是业务无关的,任何系统,只要有需要都可以引用。
二、代码风格
2.1 命名规范
https://mp.weixin.qq.com/s/WLHXrdfKc71b0EU0vi09gA
类名使用名词或者形容词 + 名词。
方法名为动词或动词短语。
包名使用小写,只能有一个自然语意的英语单词。包名使用单数,但如果类名有复数含义,则可以使用复数。
抽象类以Base/Abstract开头;异常类以Exception结尾;测试类以被测类名开头,Test结尾;枚举采用Enum结尾。
2.2 Google Java编程规范
源文件结构
1、许可证或版权信息(如有需要)
2、package语句
3、import语句
4、一个顶级类(只有一个)
注:以上每个部分之间用一个空行隔开
类成员顺序
1、变量
2、构造方法
3、公有方法
4、getter/setter方法
5、私有方法
注:重载方法永不分离。
换行
一般情况下,一行长代码超出列限制(80或100个字符),我们就需要将其分为多行。
换行的基本准则是:更倾向于在更高的语法级别处断开。
- 如果在非赋值运算符处断开,那么在该符号前断开(比如+,它将位于下一行)。
- 如果在赋值运算符处断开,通常的做法是在该符号后断开(比如=,它与前面的内容留在同一行)。这条规则也适用于 foreach 语句中的分号。
- 方法名或构造函数名与左括号留在同一行。
- 逗号(,)与其前面的内容留在同一行。
换行时,至少缩进4个空格。
空行
以下情况需要使用一个空行:
- 类内连续的成员之间:字段,构造函数,方法,嵌套类,静态初始化块,实例初始化块。
- 在函数体内,语句的逻辑分组间使用空行。
- 类内的第一个成员前或最后一个成员后的空行是可选的(既不鼓励也不反对这样做,视个人喜好而定)。
变量声明
不要组合声明,例如:
int a,b = 0;
变量需要使用时才声明
2.3 p3c规范总结
https://www.jianshu.com/p/329dd85cde4f
2.4 Effective Java总结
https://www.jianshu.com/p/61e8b5b96e98
三、单元测试
单元测试是针对程序的最小单元来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。一个单元可能是单个程序、类、对象、方法等。
3.1 为什么要写单元测试
提高代码质量
对一个单元进行测试时,需要将其隔离外部的依赖(数据库、第3方接口),保证外部依赖不影响当前单元的逻辑。
正因为如此,他会促进我们对工程进行组件化拆分,整理工程依赖关系,更大程度减少代码耦合。
提升重构自信心
重构,每个开发者都会经历,重构后把代码改坏了的情况并不少见。以往,写完一个框架,运行一下,没什么问题,完事;由于最初的框架并不是你写的,可谓牵一发动全身,你改1个方法导致整个框架运行失败。有了单元测试后,我们重构时自然就会多一分勇气。
测试驱动开发(TDD):
测试驱动开发是戴两顶帽子思考的开发方式:先戴上实现功能的帽子,在测试的辅助下,快速实现其功能;再戴上重构的帽子,在测试的保护下,通过去除冗余的代码,提高代码质量。测试驱动着整个开发过程:首先,驱动代码的设计和功能的实现;其后,驱动代码的再设计和重构。
3.2 单元测试的原则
AIR原则
A:Automatic(自动化)
I:Independent(独立性,不同的单元测试之间要互相独立)
R:Repeatable(可重复执行)
BCDE原则
编写单元测试时要保证测试粒度足够小,这样有助于精确定位问题,用例默认是方法级别的。单测不负责检查跨类或者跨系统的交互逻辑,那是集成测试需要覆盖的范围。编写单元测试用例时,为了保证被测模块的交付质量,需要符合BCDE原则。
- B: Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。
- C: Correct,正确的输入,并得到预期的结果。
- D: Design,与设计文档相结合,来编写单元测试。(没有设计文档,不懂这条什么意思)
- E: Error,单元测试的目标是证明程序有错,而不是程序无错。为了发现代码中潜在错误,我们需要编写测试用例时,有一些强制的错误输入(如非法数据、异常流程、非业务允许输入等)来得到预期的结果。
使用Mock对象
由于单元测试只是系统集成测试前的小模块测试,有些因素往往是不具备的,因此需要进行Mock,例如:
(1)功能因素。比如被测试方法内部调用的功能不可用。
(2)时间因素。比如双十一还没有到来,与此时间相关的功能点。
(3)环境因素。政策环境,如支付宝政策类新功能;多端环境,如PC、手机等。
(4)数据因素。线下数据样本过小,难以覆盖各种线上真实场景。
(5)其他因素。为了简化测试编写,开发者也可以将一些复杂的依赖采用Mock方式实现。
优秀的单元测试
(1)单元测试是“白盒测试”,应该覆盖各个分支流程、异常条件。
(2)单元测试面向的是一个单元(Unit),是由Java中的一个类或者几个类组成的单元。
(3)单元测试的运行速度一定要快!
(4)单元测试一定是可重复执行的!
(5)单元测试之间不能有相互依赖,应该是独立的!
(6)单元测试代码和业务代码同等重要,要一并维护!
3.3 怎样写
结合到本系统,普通的增删改查这样过于简单的功能就不需要进行测试了;
我们主要是对Manager层和Service层这两层进行测试,因为这两层主要设计到了数据的处理。
Service层的单元测试引用Manager层的Mock对象,Manager层引用Dao层的Mock对象。
如果像mms那样具有复杂的逻辑,我们就要将其进一步拆分成很小的单元进行测试。
Demo
@ActiveProfiles("prod")
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ArsServiceApplication.class)
public class ProjectReviewInfoDmnImplTest {
@Autowired
private IProjectReviewInfoManager projectReviewInfoDmn;
@MockBean
private VProjectReviewInfoRepository vProjectReviewInfoRepository;
// 湖州市区域code
private String regionCode = "330500000000";
@Test
public void findProjectReviewInfo() {
this.mockFindProjectReviewInfo();
// 湖州市及其下面层级的区域code
List<String> subRegionCodeList = JsonUtils.toList("[\"330500000000\",\"330501000000\",\"330502000000\",\"330503000000\",\"330521000000\",\"330522000000\",\"330523000000\"]", String.class);
CommonReviewTaskQuery commonReviewTaskQuery = new CommonReviewTaskQuery();
commonReviewTaskQuery.setAreaLevel(StatusEnum.AreaLevelEnum.CITY.desc());
commonReviewTaskQuery.setPlanType(StatusEnum.PlanTypeEnum.LAND_SPACE_PLAN.desc());
commonReviewTaskQuery.setRolesName(Collections.singletonList("市总规科"));
commonReviewTaskQuery.setRegionCodeList(subRegionCodeList);
commonReviewTaskQuery.setKeyword("湖州");
commonReviewTaskQuery.setTaskAreaLevel(StatusEnum.AreaLevelEnum.CITY.code());
commonReviewTaskQuery.setQueryApprovalStage(false);
List<VProjectReviewInfo> result = projectReviewInfoDmn.findProjectReviewInfo(commonReviewTaskQuery);
Assert.assertEquals(JsonUtils.toString(result), "xxx");
}
@SuppressWarnings("unchecked")
private void mockFindProjectReviewInfo() {
String result = "xxx";
Mockito.when(vProjectReviewInfoRepository.findAll(ArgumentMatchers.any(Specification.class), ArgumentMatchers.any(Sort.class)))
.thenReturn(JsonUtils.toList(result, VProjectReviewInfo.class));
}
}
3.4 总结
单元测试确实会带给你相当多的好处,但不是立刻体验出来。正如买重疾保险,交了很多保费,没病没痛,十几年甚至几十年都用不上,最好就是一辈子用不上理赔,身体健康最重要。单元测试也一样,写了可以买个放心,对代码的一种保障,有bug尽快测出来,没bug就最好,总不能说“写那么多单元测试,结果测不出bug,浪费时间”吧?
以下是个人对单元测试一些建议:
- 越重要的代码,越要写单元测试;
- 代码做不到单元测试,多思考如何改进,而不是放弃;
- 边写业务代码,边写单元测试,而不是完成整个新功能后再写;
- 多思考如何改进、简化测试代码。
四、重构
https://www.jianshu.com/p/e5276d50a7b5
五、日志
日志规约
本文参考文章
第一部分
1、应用分层模型
2、你的项目应该如何正确分层?
3、总结代码风格
4、到底需不需要Manager层?
第二部分
1、码出高效:Java开发手册
2、总结代码风格
3、Google Java 编程规范(中文版)
第三部分
1、谈谈为什么写单元测试
2、码出高效:Java开发手册
3、Mockito与PowerMock的使用基础教程
定期对公司项目进行基础代码的重构。合理的拆分业务无关的基础代码。
最好不要直接引用三方库,进行再次的封装
更新代码时同时更新注释和单元测试
尽量少写代码(lombok)
pom文件的管理(待google)
防御式编程:不要相信任何外来参数
类、变量命名
日志、状态码