程序员社区

辅助审查系统的代码书写规范


title: 辅助审查系统的代码书写规范
date: 2019/10/26 09:43
remark: 本系统采用SpringBoot + Dubbo进行开发


前言

近期在重构辅助审查系统,发现随着项目的发展,代码变得异常混乱,完全违背了当初定下来的规范,当然这其实是无法避免的,毕竟时间紧任务重,哪里有时间让你深思熟虑的考虑这段代码该怎样写。

之前的代码规范只在自己的心中,随着时间就会慢慢遗忘,在写新的代码的时候,可能就想不到那么多,从而导致代码的“味道”越来越差,由此我决定花一天时间,将其落实到纸上,日后写代码的时候可以看一下,尽量保证代码的味道不要太差。

当然,由于项目时间紧任务重,可能没办法所有的代码都按照规范来写,有的时候只能寻找一些捷径,这样也是不可避免的;希望大家日后有时间的时候,可以按照本规范,将违背了规范的地方进行重构。

注:由于辅助审查系统的特殊性,本规范可能不适用于其它系统。

一、工程架构模型

1.1 如何分层

辅助审查系统的代码书写规范插图
p3c规范中的分层模型

本系统采用的分层结构与p3c规范中的基本相同。

Dao层

数据访问对象(Data Access Object),用于对数据库进行访问,负责数据的CURD。

当然 Dao 不仅限于与数据库进行交互,假如日后系统引入的ElasticSearch、Mongo 甚至Redis,我认为都可以定义一个Dao对其进行访问。

这样的定义,可以将数据的CURD和业务逻辑进行分离。

Manager层

p3c规范中对其定义如下:

  1. 对第三方平台封装的层,预处理返回结果及转化异常信息
  2. 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理
  3. 与 DAO 层交互,对多个 DAO 的组合复用

由这个定义并结合我们的系统,可以得出Manager层主要作用为:

  1. 可以通过 Manager 层调用第三方服务(指标系统、全文检索服务,因为他们是基础服务,所以不要在 web 层调用他们),对返回结果进行简单的处理之后返回 Service 层。
  2. 如果需要对基础服务进行调用,并将其结果处理后入库(例如:档案管理系统、模型系统)这种用法时,直接通过 Dao 层进行保存。
  3. 可以将 Manager 理解为对通用逻辑的封装,避免 Service 与 Service 进行相互调用,以及对通用逻辑的管理。

在开发中,我们经常会遇到 AService 中的某个业务可以提供给 BService 调用,从而让 BService 调用 AService 的方法,认为是 Service 之间具有共同的业务。其实 Service 之间没有共同的业务,而是具备通用的逻辑,这时应该将其抽离出来放在 Manager 中。无论何种工程架构都好,我都不赞同 Service 与 Service 之间的相互调用。

  1. 如果2个(或2个以上)表之间有一定的关联关系(一对多、多对多)并经常一起使用,则通过一个 Manager 对他们进行访问。
  2. 可以在这层加入缓存(当然我们体量小,一般只在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层是通用的逻辑

层间对象传递

辅助审查系统的代码书写规范插图1
领域对象在不同层之间的传递

其中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个字符),我们就需要将其分为多行。

换行的基本准则是:更倾向于在更高的语法级别处断开。

  1. 如果在非赋值运算符处断开,那么在该符号前断开(比如+,它将位于下一行)。
  2. 如果在赋值运算符处断开,通常的做法是在该符号后断开(比如=,它与前面的内容留在同一行)。这条规则也适用于 foreach 语句中的分号。
  3. 方法名或构造函数名与左括号留在同一行。
  4. 逗号(,)与其前面的内容留在同一行。

换行时,至少缩进4个空格。

空行

以下情况需要使用一个空行:

  1. 类内连续的成员之间:字段,构造函数,方法,嵌套类,静态初始化块,实例初始化块。
  2. 在函数体内,语句的逻辑分组间使用空行。
  3. 类内的第一个成员前或最后一个成员后的空行是可选的(既不鼓励也不反对这样做,视个人喜好而定)。

变量声明

不要组合声明,例如:

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)

防御式编程:不要相信任何外来参数

类、变量命名

日志、状态码

辅助审查系统的代码书写规范插图2
这是我现在的想法(2020.06.02)
赞(0) 打赏
未经允许不得转载:IDEA激活码 » 辅助审查系统的代码书写规范

一个分享Java & Python知识的社区