Java开发 高可维护性代码规范

目录

理念与目标

实施目的

关于阿里代码规约

术语

接口方法

功能点方法

子功能点方法

业务逻辑片段方法

实体转换方法

业务方法

rpcservice层

VO(View Object)

BO(Business Object)

DTO(Data Transfer Object)

PTO (Param Transfer Object)

PO(Persistent Object)

分层规范

controller层规范

强制执行

建议执行

service层规范

强制执行

建议执行

rpcservice层规范

强制执行

建议执行

dao层规范

强制执行

建议执行

实体规范

VO规范

强制执行

建议执行

BO规范

强制执行

建议执行

DTO规范

强制执行

建议执行

PTO规范

强制执行

建议执行

PO规范

强制执行

建议执行

其他规范

强制执行

建议执行

参考资料


理念与目标

实施目的

编写高可维护性的代码,以便于随时进行必要的重构,以应对需求的变化、人员的变更等情况

关于阿里代码规约

是参考之一,但未必在全部书写细节上都迷信于阿里的规约,最高原则是保持代码的高可维护性,以应对需求变化、人员更换等情况

术语

接口方法

产品线对外开放的http服务,位于XXXController中。

功能点方法

产品线中各个功能点的体现,如“功能点:用户登录”、“功能点:添加商品”、“功能点:从购物车下订单”等。

方法的定义位于IXXXService.java接口中,实现位于XXXServiceImpl.java中。

功能点方法是public方法。

子功能点方法

子功能点参与组成一个功能点,便于其他功能点方法、其他子功能点方法调用。

如:功能点为“首页tab”,则此功能点由“子功能点:第一张tab内容”、“子功能点:第二张tab内容”、“子功能点:第三张tab内容”、“子功能点:第四张tab内容”,共4个子功能点组成

方法直接实现在XXXServiceImpl.java中(或者XXXRPCService.中),不在IXXXService.java  这一interface中定义。

子功能点方法是public方法。

业务逻辑片段方法

业务逻辑片段参与组成一个功能点或一个子功能点,是功能点方法中相对独立的一段逻辑抽出来而形成的,便于其他功能点方法、其他子功能点方法调用。

方法直接实现于XXXServiceImpl.java中(或者XXXRPCService.java中)。

业务逻辑片段方法是private方法

实体转换方法

实现各种实体之间的转换,推荐写在实体中,尽量不要写在Service类中

业务方法

功能点方法”、“子功能点方法”、“业务逻辑片段方法”的统称

rpcservice层

与service层处于并列位置(可参考“微服务架构中的实体类分层及数据传输示意图”),其中以子功能点方法的形式封装了rpc接口的业务逻辑。

当使用feign开放rpc接口时,只有XXXRPCService,不再为其定义interface。

当使用dubbo开放rpc接口时,有XXXRPCServiceImpl与IXXXRPCService的组合

VO(View Object)

VO对应的是一个前端提交过来的结构化信息,如页面的form

BO(Business Object)

BO意为“业务逻辑实体”,其中封装了一个service层的功能点方法返回的信息

DTO(Data Transfer Object)

DTO的作用:

  1. 盛装service层的子功能点方法业务逻辑片段方法的复合型返回值(采用连表查询时,对应于一张单表的PO,字段通常不够用)。此时原则上可以随意扩展其属性

  2. 盛装调用产品线外部接口(可能是第三方服务的接口,也可能是公司内部服务的接口)返回的复合型返回值。此时原则上不可以随意扩展其属性,因为需要对应外部接口的文档

3. 盛装调用rpc接口返回的复合型返回值。此时原则上可以随意扩展其属性

PTO

PTO的作用:

1. 盛装rpcservice层的子功能点方法的复合型入参,即rpc服务的复合型入参。此时原则上可以随意扩展其属性

2. 盛装调用外部接口的复合型入参。此时原则上不可以随意扩展其属性,因为需要对应外部接口的文档

PO(Persistent Object)

PO对应数据库中的一张表

分层规范

controller层规范

强制执行

同一个工程中,注意controller不要重名。若命名时需要使用相同的词语,要注意不要破坏类名的后缀

Controller中的方法,即使入参没有必要封装成复合类型的对象,入参的校验工作也要放在Controller中的private方法中进行

controller层的各个public方法(即对外开放http接口的各个接口方法),其中包含至多三件事:

1) 校验入参

2) 调用service层方法

3) 读写cookie、读threadlocal、读写HttpServletRequest、读写HttpServletResponse

接口方法只体现正常流程的终结而不体现异常流程的终结

controller的方法中,只会出现类似HttpRespBody.successXXX(...)的内容

类似于HttpRespBody.errorXXX(...)的内容,不会出现在controller层的接口方法中,而是写在统一异常处理器中,统一异常处理器由架构组维护,业务逻辑编写者无需操心

接口方法中,只有需要针对service层方法的返回值的内容进行判断时(比如不同的返回值决定了接口调用成功的不同提示语),才可能在controller层的接口方法中写if-else或switch

对service层方法返回值的使用:

95%左右的情况下,controller层的接口方法中不会对service层的功能点方法的返回值进行判断,而只是直接进行使用

5%情况,controller层的接口方法不直接使用service层的功能点方法的返回值,而判断一下返回哪个成功的结果,然后再使用:

POST请求使用json形式传递入参

上传文件的post请求使用k-v形式

GET请求使用 k-v形式(即表单形式) 传递入参

建议执行

Controller中的接口方法,入参优先使用复合类型的对象,即封装成XXXVO

service层规范

强制执行

XXXService中的功能点方法,返回值可以是void、简单包装类类型(Integer、Boolean之类),如果要返回复合类型,必须是XXXBO 或 Collection<XXXBO>

**.module包下按照“功能点大类别相同则放在同一包下”的原则继续划分包。每个package内,强相关的功能点放置在同一个service类中

在 IXXXService  interface的类级别注释中,写清楚本interface中包含的功能点;

在实现类 XXXServiceImpl implements IXXXService 的类级别注释中写清楚本类中包含的子功能点

service层的实现类 XXXServiceImpl.java 中有且只有四种方法

其中两种是public方法

另外两种是private方法

注:此条规范是为了帮助开发者编写短小、职责单一、易于被复用的方法

业务逻辑字里行间的注释不求多但求精。XXXServiceImpl中的相关方法的内部,要分段写明第一段、第二段...第n段代码的作用是什么。

本条规范的目的是为了确保各业务逻辑之间尽量不要杂糅在一起,比如要避免出现那种贯穿在各个逻辑段落之中的反复被赋值的变量。抽取子功能点方法业务逻辑片段方法时也更容易一些

service层不得做 “不具备业务意义的入参校验” (比如非空判断、email格式正确、电话号码长度正确等),能够传到service层的入参,在书写层面,均被认为是规范的

业务方法功能点方法子功能点方法业务逻辑片段方法)的长度,原则上不许超过75行(项目从0到1时要限制在55行以内)。

日志、空行、折行、链式调用折行、注释不算行数

写业务逻辑的时候,有时候需要使用(并发)设计模式。一次设计用到的相关的类,要单独放置到一个形如**.service.impl.design.dn的package中

不要在service层方法的入参中写request、response,如果需要在service层处理request、response

可以使用

((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest()

service层的方法,访问控制级别升级(private -> public)遵循如下规则:

1) 子功能点不可升级为功能点

2) 业务逻辑片段方法可以升级为子功能点方法

注:本条规范描述得可能比较抽象,其实目的在于,有时候需要将private的方法变成public访问级别,以便于其他方法共用之

编写自己的方法时,对已有方法可能会进行“调用”或“复制其内容”,此两种行为均被允许,但要有所取舍

各种书上通常认为,复制其他方法中的业务逻辑到自己的方法中是错误的,但本规范允许此做法

只有你非常确定其他方法中的业务逻辑一定是你需要的(即使日后作者会对其重构,甚至删除它),你才可以调用其他人编写的业务方法,否则不妨复制粘贴其中的片段到你自己编写的方法中

本规范明令禁止为了实现自己的业务逻辑,不经过研发经理批准就改动已有方法的行为 —— 你可以“调用”、你可以“复制其中的片段”,但是原则上不能改别人的方法,即使微调返回值中的一个field也不行

建议执行

尽量不要写多层if,以至于最终代码形成开口向左的倒V形。要合理组织逻辑,尽量将需要以“倒V形”表述的逻辑,以“在逻辑段前,有多个判断非法的if”之形式进行表述

如:

不要如下所示编写程序:

public void bizMethod() {

    // 开口向左的倒V代码,难于重构

    if(条件1满足) {

        if(条件2满足) {

            if(条件3满足 && 条件4满足) {

                if(条件5满足 || 条件6满足) {

                   if(条件7满足) {

                       // do sth.

                   }

                }

            }

        }

    }

}

而要写成多个卫语句Guard Condition的形式:

public void bizMethod() {

    if(!条件1满足) {// guard condition

        throw new ProjException("msg...");

    }

    if(条件2满足) {// guard condition

        return;

    }

    if(!条件3满足 || !条件4满足) {// guard condition

        throw new ProjException("msg...");

    }

    if(!条件5满足 && !条件6满足) {// guard condition

        throw new ProjException("msg...");

    }

    if(!条件7满足) {// guard condition

        throw new ProjException("msg...");

    }

    // do sth.

}

一个 IXXXService.java中,最多包含不超过3个功能点方法的定义

注:事实上,本条规范的目的是为了防止 XXXServiceImpl.java 类的行数过多,具体放置几个功能点,可由研发经理灵活掌握

抽取private方法,是为了封装一段业务逻辑,以利于其调用者自身不至于有太多的行数。切忌封装了一个private方法,但其不是一个完整的业务逻辑片段,即“为了封装而将数行代码封装,封装者自己也不知道这段代码目的为何”

rpcservice层规范

强制执行

**.rpc.abc 包下放置本微服务为微服务abc提供的rpc接口

rpcservice层的XXXRPCService.java 中有且只有三种方法

其中一种是public方法

public 返回值 子功能点方法(入参…) {

    // 内容...

}

其中

入参类型:

基本类型包装类 | Collection<基本类型包装类> | Param | Collection<Param>

返回值类型:

基本类型包装类 | Collection<基本类型包装类> | DTO | Collection<DTO> | void

另外两种是private方法

private 返回值 业务逻辑片段方法(入参) {

    // 内容...

}

private 某实体 | Collection<某实体> 实体转换方法(某实体 | Collection<某实体>) {

    // 内容...

}

实体转换方法尽量不要写在XXXRPCService.java中,尽量写在entity中

注:此条规范是为了帮助开发者编写短小、职责单一、易于被复用的方法

建议执行

dao层规范

强制执行

mapper中自定义的方法必须以select、count、insert、update、delete开头,其他层的方法不可以这些词开头

禁止写 SELECT *  需要什么字段查什么字段

自定义的dao层方法的返回值只能是: 简单类型包装类 | Collection<简单类型包装类> | XXXPO | Collection<XXXPO> | XXXDTO | Collection<XXXDTO> | XXXBO

建议执行

单表CURD不建议自己写

实体规范

VO规范

强制执行

VO中如要使用内部类,原则上使用静态内部类。VO中定义内部类,只有一层,即不要在VO的内部类中再定义内部类了

controller层中,不同的接口方法使用的入参VO原则上禁止复用,要新建

在VO类上把注释写好,说明此VO适用于哪个接口

接口传参校验,如果使用hibernate validator,注解加在VO的field上。如果不想用或不能用hibernate validator,在controller中编写private方法进行校验

如果前端传来的参数命名不符合驼峰命名,要在field上使用jackson提供的@JsonProperty注解对应前端传来的参数。在后端工程的代码中,变量必须遵循驼峰命名

实体转换方法,如若写在实体中,则必然写在VO、BO、PO中,不要写在DTO和Param中

建议执行

lombok注解 搭配建议:

类上:

@Getter

@Setter

@ToString

@Builder//内部类不加

@NoArgsConstructor

@AllArgsConstructor

public class HouseVO implements java.io.Serializable {

带有初始值的field上:

@Builder.Default

private Integer pageSize = 10

实体转换方法,在service层中写private方法是可以的,但推荐写在bean中。且推荐优先使用重载或重新定义新方法,而非使用已有的实体转换方法

BO规范

强制执行

任何的BO上面要有注释,说明此BO适用于哪个功能点

如果前端需要不满足驼峰规则的属性命名,或者前端要求的返回值命名与本地属性不同,要在field上使用jackson提供的@JsonProperty注解对应前端需要的属性。在后端工程的代码中,变量必须遵循驼峰命名

BO中使用内部类,原则上使用非静态内部类。在BO中定义内部类时,只有一层,即不要在BO的内部类中再定义内部类了

要 implements java.io.Serializable

实体转换方法,如若写在实体中,则必然写在VO、BO、PO中,不要写在DTO和Param中

建议执行

实体转换方法,推荐优先使用重载或重新定义新方法,而非使用已有的转换方法(除非你可以完全承担调用其他方法后,该方法被作者修改的后果)

lombok、jackson注解 搭配建议

@Getter

@Setter

@ToString

@Builder//内部类、抽象类不加

@NoArgsConstructor

@AllArgsConstructor

@JsonInclude(JsonInclude.Include.NON_NULL)

面对返回信息量比较大、逻辑分支较多的业务场景(如首页选项卡),可以使用子类BO作为子功能点方法的返回值

DTO规范

强制执行

当DTO用作盛装调用外部接口(可能是第三方服务的接口,也可能是公司内部服务的接口)返回的复合型返回值之时,结构可能比较复杂。如果需要使用内部类,原则上使用静态内部类。

此种情况下,原则上不可以随意扩展其属性(因为这是对应于第三方接口的接口文档的,而不是给你的rpc接口使用的)

DTO中定义内部类,只有一层,即不要在DTO的内部类中再定义内部类了 —— 属性可以多层嵌套,但是定义不要搞多层嵌套

P.S.记得类上写好注释

对于yyy-microsvc工程,若XXXDTO用作盛装rpc接口返回的复合型返回值之时,则此XXXDTO必定处于yyy-microsvc-api工程之中。不可使用位于其他工程中的XXXDTO(如xxx-microsvc-api工程中另有一个XXXDTO类),即使类名字是相同的也不行

当承接产品线外部接口返回值的时候,如果返回值中有不规范的属性命名,则在DTO的field上使用jackson提供的@JsonProperty注解,用以对应调用外部接口得到的返回的值。在后端工程的代码中,变量一定要遵循驼峰命名

要 implements java.io.Serializable

DTO中不包含任何实体转换方法实体转换方法应写于BO、VO、PO中

建议执行

lombok注解 搭配建议

@Getter

@Setter

@ToString

@Builder//内部类不加

@NoArgsConstructor

@AllArgsConstructor

@JsonInclude(JsonInclude.Include.NON_NULL)

对于盛装service层的 子功能点方法业务逻辑片段方法 的复合型返回值的DTO,尽量使用以数据库表名来命名的DTO

Param规范

强制执行

充当rpcservice层的子功能点方法业务逻辑片段方法的复合型入参时,原则上可以随意扩展其属性

充当调用外部接口的复合型入参时,结构可能比较复杂。如果需要使用内部类,原则上使用非静态内部类。原则上不可以随意扩展其属性(因为这是对应于外部接口的接口文档的)

Param中定义内部类,只有一层,即不要在Param的内部类中再定义内部类了

若某个微服务abc-microsvc中的某个 子功能点方法业务逻辑片段方法 的入参形式为 (XXXParam),则XXXParam必定处于abc-microsvc-api工程之中

不可使用其他XXXParam(如def-microsvc-api工程中也有一个叫XXXParam的类),即使类名字是相同的也不行

在充当外部接口的复合型入参的时候,如果入参中有不规范的属性命名,则在Param的field上使用jackson提供的@JsonProperty注解,用以对应调用第三方接口时的传参。在后端工程的代码中,变量一定要遵循驼峰命名

要 implements java.io.Serializable

Param中只有field及相关的lombok方法,不包含任何实体转换方法方法,实体转换方法应写于BO、VO、PO中

建议执行

lombok注解 搭配建议

@Getter

@Setter

@ToString

@Builder//内部类不加

@NoArgsConstructor

@AllArgsConstructor

对于充当rpcservice层的 子功能点方法业务逻辑片段方法 的复合型入参的Param,尽量使用数据库表名来命名Param,这样有利于其他人复用、扩展你定义的Param

PO规范

强制执行

不要 implements java.io.Serializable

实体转换方法,如若写在实体中,则必然写在VO、BO、PO中,不要写在DTO和Param中

建议执行

实体转换方法,在XXXService中写private方法是可以的,但推荐将实体转换方法写在PO中,这样有利于防止一时失误,将业务逻辑写入实体转换方法之中

且推荐优先使用重载或重新定义新方法,而非使用已有的实体转换方法。除非你非常确定,现存的实体转换方法一定是你需要的,即使这个方法被作者改动后仍然是你需要的

lombok注解 搭配建议

类上:

@Getter

@Setter

@ToString

@Builder

@NoArgsConstructor

@AllArgsConstructor

其他规范

强制执行

lombok注解,暂时禁止使用@Data、@EqualsAndHashCode

对于想要废弃的方法或者类,如果已经被别人使用了,可以使用@Deprecated标注,观察一段时间以后,经研发经理同意方可删除

类的命名,其后缀都是有意义的,不能像定义名称相似的方法时候,那样随意地在后面加数字。对于方法的命名没有过多限制,如果需要,随意在同名的方法名后面加数字就好

所有实体的基本数据类型的field,一律使用基本数据类型的包装类型,以避免给属性赋值为null之时报错

如:

不能使用 private int age;

要使用 private Integer age;

Const.java中的常量优先使用基本类型,而非基本类型的包装类

应使用:

public static final int DEFAULT_PAGE_SIZE = 10;

而非:

public static final Integer DEFAULT_PAGE_SIZE = 10;

代码提交前,必须自测(跑起来自行调用接口,或运行个UT),以避免提交代码不全,导致其他人编译报错的情况

警惕“魔术常量”,要将其定义为enum,或者放进Const.java中——以常量或枚举明确其业务意义

异常流程中不要包含业务逻辑。出现异常了,不是进入了一个分支、一种情况,而是要抛出一个表征不正常情况的结果 —— “业务逻辑至此立刻终止了”

使用eclipse时,对于可以序列化的类,不要生成serialVersionUID,放任警告就好

实体转换方法中,绝对不可包含业务逻辑。

除了PO、BO、VO、DTO、Param之外的其他类型的实体(比如,你自己在使用设计模式时,定义的一些实体)中,是否要包含业务逻辑,可自己权衡。

实体转换方法的形式可以是一个实例方法,也可以是一个构造器。

实体转换方法的形式为构造器时,请注意不要泄露this指针。

实体转换方法(无论是否写在实体中)以单词convert开头。

原则上不允许编写类方法形式的实体转换方法,使用构造器或实例方法已足够。

有时进行git对比时,发现本地代码与版本库中完全一致,但是还是显示有difference,此时不可无脑提交,要查看是否是换行符没有规范为unix式

格式化代码时,严禁全局格式化,仅格式化必要部分即可。

推荐手动格式化,市面上没有找到特别理想的格式化模板

在每个XXXServiceImpl.java文件中,会实现数个功能点。一个XXXServiceImpl文件对应一个Const.java文件,其中以功能点为维度划分内部类,放置业务逻辑中用到的常量,鼓励常量之间互相引用,以体现功能点、子功能点之间的某种关联性

在每个XXXRPCServiceImpl.java文件中,会实现数个子功能点。一个XXXRPCServiceImpl文件对应一个Const.java文件,其中以子功能点为维度划分内部类,放置业务逻辑中用到的常量,鼓励常量之间互相引用,以体现功能点、子功能点之间的某种关联性

对于所有微服务工程都可能用到的,且其内容不会变化的配置(如“图片服务器的地址”、“某第三方厂商的接口地址”),放置在如下位置,使用时用@Value(“${xxx.fff.kkk}”)注入其值

建议执行

当你决定吃掉异常时要慎重

当你抛出的异常不是org.ygf.commons.core.exception.SysException时,要慎重

在编写业务逻辑时,由于本规范允许复制-粘贴代码段,可以禁用idea的重复代码段提示功能,以免干扰判断

rpc接口建议返回XXXDTO,而非简单类型包装类,否则不利于做服务降级

允许使用行内注释,尤其是对于变量,可以禁用阿里的代码规约插件,以免干扰判断

BO、PO、VO与DTO、Param转换的方法,即实体转换方法,不要写在DTO,Param中(其实写了你就会发现,大概率编译报错)

方法内的局部变量,其作用域要尽量短——没有用到它的时候,就不要定义出来,因为定义任何的变量都是要开辟内存空间的,开辟出内存空间却不使用/甚至用不到,不是好的习惯

原则上不要在类级别上加@Transactional,在需要控制事务的方法上加此注解

参考资料

[1]《阿里巴巴Java开发手册(Effective Coding)》 杨冠宝(孤尽) 编著

[2]《重构 改善既有代码的设计(Refactoring Improving the Design of Existing Code)》 [美]Martin Fowler 著, 熊杰 译

[3]《代码不朽 编写可维护软件的10大要则(Building Maintainable Software)(JAVA版)》 [荷]Joost Visser 著, 张若飞 译

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值