项目经验_之_开发规范_编码规约
前言
Github:https://github.com/HealerJean
一、命名风格
1、类名 命名
类名使用
UpperCamelCase
风格,必须遵从驼峰形式,但以下情形例外:(领 域模型的相关命名)DO
/BO
/DTO
/VO
/DAO
影响: 代码可读性差
正例: ForceCode / UserDO / HtmlDTO /TcpUdpDeal / TaPromotion
反例: forcecode / UserDo / HTMLDto /TCPUDPDeal / TAPromotion
2、方法名、参数名、成员/局部变量 命名
1.1.2、方法名、参数名、成员/局部变量统一使用
lowerCamelCase
,必须遵从驼峰形式影响:反序列化或者
Lombok
中@Data
赋值失败
正例: localValue / testMethod()
3、抽象类 命名
抽象类命名使用
Abstract
或Base
开头影响: 代码可读性差
正例: AbstractActionDemo
4、单元测试类 命名
单元测试类命名以它要测试的类的名称开始,以
Test
结尾影响: 代码可读性差
正例: DemoTest
5、下划线和美元符号 使用
代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束
影响: 代码可读性差
反例: _name / __name / $name / name_ / name$ / name__
6、异常类 命名
异常类命名使用
Exception
结尾影响: 代码可读性差
CacheDemoException
7、常量 命名
常量命名应该全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长
影响: 代码可读性差
正例:MAX_STOCK_COUNT / CACHE_EXPIRED_TIME
反例:MAX_COUNT / EXPIRED_TIME
8、 Service
和 DAO
类 使用
对于
Service
和DAO
类,基于SOA
的理念,暴露出来的服务一定是接口,内部的实现类用Impl
的后缀与接口区别影响: 代码可读性差
正例:public class DemoServiceImpl implements DemoService
9、包名 命名
包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式
影响: 代码可读性差
正例: com.jd.mpp.util/com.jd.tddl.domain.dto
10、POJO
类使用
1)布尔类型 命名
POJO
类中的任何布尔类型的变量,都不要加is,否则部分框架解析会引起序列化错误影响: 部分框架解析会引起序列化错误
正例:Boolean success
反例:定义为基本数据类型Boolean isDeleted的属性,当它的方法也isDeleted()时,框架在反向解析的时候,“误以为”对应的属性名称是deleted,导致属性获取不到,进而抛出异常
2)禁止在 POJO
类中,同时存在对应属性 xxx
的 isXxx()
和 getXxx()
方法
禁止在
POJO
类中,同时存在对应属性xxx
的isXxx()
和getXxx()
方法影响:框架在调用属性
xxx
的提取方法时,并不能确定哪一个方法一定是被优先调用到的
3)类型与中括号紧挨相连来表示数组
类型与中括号紧挨相连来表示数组
影响: 代码可读性差
正例:定义整形数组int[] arrayDemo;
反例:在main参数中,使用String args[]来定义
11、如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式
将设计模式体现在名字中,有利于阅读者快速理解架构设计理念。
正例:
public class OrderFactory;
public class LoginProxy;
public class ResourceObserver;
12、枚举命名
枚举类名带上
Enum
后缀,枚举成员名称需要全大写,单词间用下划线隔开
13、方法命名
场景 | 命名 |
---|---|
增 | saveBean |
删 | deleteBean |
该 | udpateBean |
查-单记录查询 | getBeane |
查-列表记录查询 | listBean |
查-分页记录查询 | pageBean |
二、注释
1、字段和方法 注释
所有的字段和方法必须要用
javadoc
注释影响:严重影响代码可读性和编码效率
2、枚举 注释
所有的枚举类型字段必须要有注释,说明每个数据项的用途
影响: 代码可读性差
public enum TestEnum {
/**
* 月份
*/
MONTH;
}
3、单行 注释
方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使用/**/注释,注意与代码对齐
影响:影响代码可读性和编码效率
4、类注释
类必须有
@author
(创建者)、@since
(创建日期)、类功能描述等注释信息
5、中英文注释
与其“半吊子”英文来注释,不如用中文注释把问题说清楚。专有名词与关键字保持英文原文即可
6、代码修改记得改注释
代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改。
说明:代码与注释更新不同步,就像路网与导航软件更新不同步一样,如果导航软件严重滞后,就失去了导航的意义
7、对于注释的要求
第一、能够准确反映设计思想和代码逻辑;
第二、能够描述业务含义,使别的程序员能够迅速了解到代码背后的信息。完全没有注释的大段代码对于阅读者形同天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路; 注释也是给继任者看的,使其能够快速接替自己的工作
三、集合
1、集合转数组
使用集合转数组的方法,必须使用
toArray(T[] array)
,传入类型完全一样的数组,大小list.size()
说明:使用
toArray
带参方法,数组空间大小的length
,
1) 等于 0
,动态创建与 size
相同的数组,性能最好。
2) 大于 0
但小于size
,重新创建大小等于 size
的数组,增加 GC
负担。
3) 等于 size
,在高并发情况下,数组创建完成之后,size
正在变大的情况下,负面影响与2相同。
4) 大于 size
,空间浪费,且在 size
处插入 null
值,存在 NPE
隐患。
正例:Integer[] b = (Integer [])c.toArray(new Integer[0]);
反例:直接使用toArray无参方法存在问题,返回值只能是Object[]类。若强转其它类型数组将出现ClassCastException错误。
Integer[] a =(Integer[])c.toArray();
2、集合初始化时,指定集合初始值大小
集合初始化时,指定集合初始值大小
说明:
HashMap
使用HashMap(int initialCapacity)
初始化,如果暂时无法确定集合大小,那么指定默认值(16
)即可。影响:浪费内存,降低性能
正例:initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即loader factor)默认为0.75,如果暂时无法确定初始值大小,请设置为16(即默认值)。
反例:HashMap需要放置1024个元素,由于没有设置容量初始大小,随着元素不断增加,容量7次被迫扩大,resize需要重建hash表。当放置的集合元素个数达千万级别时,不断扩容会严重影响性能。
四、OOP
1、比较相等
1)Object的equals
Object
的equals
方法容易抛出空指针异常,应使用常量或确定有值的对象来调用equals
影响:导致业务逻辑错误
正例:"test".equals(object); Objects.equals(a, b);
反例:object.equals("test");
2)包装类对象之间值的比较,全部使用 equals
方法比较
所有的包装类对象之间值的比较,全部使用
equals
方法比较影响:导致业务逻辑错误 (对于
Integer
在-128至127之间的值会在缓存里对象复用,区间外数据会产生新对象)
正例:Objects.equals(a, b);
反例:Integer.valueOf(a) == Integer.valueOf(b);
2、基本数据类型与包装数据类型的 使用
1、 所有的
POJO
类属性必须使用包装数据类型;2、
RPC
方法的返回值和参数必须使用包装数据类型3、所有的局部变量推荐使用基本数据类型
说明:
POJO
类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何NPE
问题,或者入库检查,都由使用者来保证
正例:数据库查询结果可能是null,因为自动拆箱,用基本数据类型接收有NPE风险。
反例: RPC方法的返回值使用基本类型,无法区分null值和0的区别
3、 DO
/ DTO
/ VO
等 POJO
类时,不要加任何属性默认值
说明:ORM框架根据列修改时,会将默认值覆盖数据库的存储值
反例: POJO类的createTime默认值为new Date(),但是这个属性在数据提取时并没有置入具体值,在更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间
4) POJO
类必须写 toString
方法
说明:使用工具类
source
>generate toString
时,如果继承了另一个POJO
类,注意在前面加一下super.toString
。打印对象信息日志,性能优于JSON
影响: 降低编码效率,影响日志打印性能
public class ToStringDemo extends Super {
private String secondName;
@Override
public String toString() {
return (
super.toString() +
"ToStringDemo{" +
"secondName=" +
secondName +
"}"
);
}
}
class Super {
private String firstName;
@Override
public String toString() {
return "Super{" + "firstName=" + firstName + "}";
}
}
5)BigDecimal
1)精度问题
禁止使用构造方法
BigDecimal(double)
的方式把double
值转化为BigDecimal
对象影响: 数据精度丢失
说明:
BigDecimal(double)
存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。如:
BigDecimal
g
=new BigDecimal(0.1f)
; 实际的存储值为:0. 100000000000000005551115123125782702118
正例:优先推荐入参为 `String` 的构造方法:
BigDecimal recommend1 = new BigDecimal("0.1");
BigDecimal recommen d2 = BigDecimal.valueOf(0.1);
或使用BigDecimal的valueOf方法,此方法内部其实执行了Double的toString,而 Double的toString按double的实际能表达的精度对尾数进行了截断。
6、方法的参数个数建议不超过5个,方便代码阅读与理解
方法的参数个数建议不超过5个,方便代码阅读与理解
影响:降低编码效率,不方便阅读和理解
7、体的行数不能多于70行
影响: 降低编码效率,不方便阅读和理解
8、强转对象操作需要判断对象类型是否匹配
说明:强转之前必须使用
instanceof
进行类型判断影响: 类型转换失败,抛出
ClassCastException
理解:个人认为大可不必
9、字符串循环拼接
字符串循环拼接,不能用 + 连接, 需要用
StringBuilder
代替,提高性能影响:浪费内存,影响性能
10、switch
控制语句
在一个
switch
块内,每个case
要么通过break
/return
等来终止,要么注释说明程序将继续执行到哪一个case
为止;在一个switch
块内,都必须包含一个default
语句并且放在最后,即使它什么代码也没有说明:注意
break
是退出switch
语句块,而return
是退出方法体。 没有break
的情况下,增加case
场景容易出现bug
11、避免采用取反逻辑运算符
’!’运算符不利于快速理解
说明:取反逻辑不利于快速理解,并且取反逻辑写法必然存在对应的正向逻辑
12、并发规约
1)线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。
如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题
2)线程池不允许使用 Executors
去创建
线程池不允许使用
Executors
去创建,而是通过ThreadPoolExecutor
的方式,这样的处理方式让写的人员更加明确线程池的运行规则,规避资源耗尽的风险
说明:Executors
返回的线程池对象的弊端如下:
1) FixedThreadPool
和 SingleThreadPool
:允许的请求队列长度为 Integer.MAX_VALUE
,可能会堆积大量的请求,从而导致 OOM
2) CachedThreadPool
:允许的创建线程数量为Integer.MAX_VALUE
,可能会创建大量的线程,从而导致 OOM
3)创建线程或线程池时请指定有意义的线程名称,方便出错时回溯
说明:创建线程池的时候请使用带
ThreadFactory
的构造函数,并且提供自定义ThreadFactory
实现或者使用第三方实现
4)避免Random
实例被多线程使用
虽然共享该实例是线程安全的,但会因竞争同一
seed
导致的性能下降
正例:可以使用 ThreadLocalRandom
public class RandomInThread extends Thread {
private Random random = ThreadLocalRandom.current();
@Override
public void run() {
long t = random.nextLong();
}
反例:
public class RandomInThread extends Thread {
private Random random = new Random();
@Override
public void run() {
long t = random.nextLong();
}
}
13、圈复杂度
圈复杂度 | 方法圈复杂度 |
---|---|
圈复杂度-高: | 方法圈复杂度大于50(等级:·BLOCKER ) |
圈复杂度-中高: | 方法圈复杂度大于40,小于等于50(等级:CRITICAL ) |
圈复杂度-中: | 方法圈复杂度大于30,小于等于40(等级:MAJOR ) |
圈复杂度-低: | 方法圈复杂度大于20,小于等于30(等级:WARNING ) |
14、避免出现重复的代码
随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。
五、异常处理
1、异常不要用来做流程控制,条件控制
异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多
2、不随意catch
catch
时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch
尽可能进行区分异常类型,再做对应的异常处理。说明:对大段代码进行
try
-catch
,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。
3、捕获异常是为了处理它
捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
4、定义时区分 unchecked
/ checked
异常
避免直接抛出
new RuntimeException()
,更不允许抛出Exception
或者Throwable
,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException
/ServiceException
等。
5、错误码还是抛异常
对于公司外的
http
/api
开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间RPC
调用优先考虑使用Result
方式,封装isSuccess()
方法、“错误码”、“错误简短信息”。
问题:关于 RPC
方法返回方式使用 Result
方式的理由:
1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
2)如果不加栈信息,只是 new
自定义异常,加入自己的理解的error
message
,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。