目录
PTO (Param Transfer Object)
理念与目标
实施目的
编写高可维护性的代码,以便于随时进行必要的重构,以应对需求的变化、人员的变更等情况
关于阿里代码规约
是参考之一,但未必在全部书写细节上都迷信于阿里的规约,最高原则是保持代码的高可维护性,以应对需求变化、人员更换等情况
术语
接口方法
产品线对外开放的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 著, 张若飞 译