一、注释
(1)写什么样的注释
写有意义的注释,而不是废话的、没有价值的注释。注释的目的是帮助读代码的人了解作者在写代码时的思想。
(2)为代码中的瑕疵写注释
(3)注释编写的步骤:
1)不管你心里再想什么,先把它记录下来
2)读一下这段注释,看看它有什么改进的地方
3)不断改进
(4)什么地方不需要注释
1)能从代码本身中快速推断的事实
2)用来装饰垃圾代码(比如拗口的方法名),实际上应该把名称修改好
(5)记录你的想法
1)为什么代码写成这样而不是另外一个样子的内在理由(指导性批注)
2)代码中的不足,使用像TODO这样的标注
3)常量背后的意义,为什么是这个值?
(6)在读者的立场思考
1)预料到代码中哪些部分会让读者产生疑惑,给它加上注释
2)为小白意料之外的行为加注释
3)在文件、类级别上使用“全局观”注释来解释所有的部分是如何一起工作的
4)用注释总结代码块,让读者不会迷茫在细节里
总结:言简意赅的注释,通过简单的注释体现代码的核心、深层思想
二、简化流程让代码易读
三、拆分又臭又长的表达式
(1)尽量不要在if语句里面写表达式,这样的代码可读性比较差,最好将表达式提出来赋值给一个中间变量,提高代码可读性
(2)反向思考,问题更加简单
以下代码是判断传入的集合是否和指定的集合有交集, 如果使用正向的思维,需要判断和比较的内容就很多。如果使用反向的思维,则只需要两行代码就可以解决问题,解决了又臭又长的代码
总结:
引入“解释变量”代替较长的子表达式,有三个好处:
1、它把巨大的表达式拆分成一个小段
2、通过简单的名字来描述一个子表达式,让代码文档化
3、它帮助读者识别代码中重要的概念
四、变量与可读性
这一行代码就是多余的
改为:
总结:
1)减少变量,通过立刻处理结果来消除中间变量
2)减少变量作用域,作用域越小越好
3)变量只写一次最好,只设置一次的变量会让代码变得更加容易理解。
五、抽离无关代码
尽量少用非逻辑,使用正逻辑,便于理解
六、好的方法
1、封装判断
好的方法是清晰易懂的,如果把解释条件意图作为函数抽离出来,用函数名把判断条件的语义显性化地表达出来,就能立即提升代码的可读性和可理解性
原始代码:
if(customer.getCrmUserId().equals(NIL_VALUE)
&& customer.getCustomerGroup() != CustomerGroup. CANCEL_ GROUP)
{
privateSea.pickUp(customer);
}
上述代码中,if后面的判断比较恶心,原因是缺少封装和合理的命名,可以使用封装判断改为:
if(canPickUpToPrivateSea())
{
privateSea.pickUp(customer);
}
private boolean canPickUpToPrivateSea(){
if(StringUtil.isBlank(this.getCrmUserId())){
return false;
}
if(this.getCustomerGroup() == CustomerGroup.CANCEL_GROUP){
return false;
}
return true;
}
重构后的代码的可读性自然提升不少。
2、方法参数
总体上说,参数越少越容易理解,方法也越容易测试和使用,如果方法需要3个以上参数,就说明其中一些参数应该封装为类。
3、短小的方法
有时保持代码的逻辑不变,只是把长方法改为多个短方法,代码的可读性就能提高很多,。超长方法是典型的代码“坏味道”,对超长方法的结构化分解是提升代码可读性最有效的方式之一。
4、职责单一
一个方法只做一件事情,也就是方法级别的单一职责原则(SRP)
遵循SRP不仅可以提升代码的可读性,还能提升代码的可复用性。因为职责越单一,功能越内聚,就越有可能被复用,这和代码的行数没有直接的关联性,但是有间接的关联性。
长方法意味着肯定需要拆分,需要用多个子函数的组合进行更好的表达。然而短小的函数并不一定就意味着不需要拆分,只要不满足SRP,值得进一步分解。哪怕分解后的子函数只有一行代码,只要有助于业务语义显性化的表达,就是值得到。
5、精简辅助代码
辅助代码就是程序运行中不可少的代码,但又不是处理业务逻辑的核心代码,比如判空,打印日志,鉴权,降级和缓存检查等。这些代码一般会在多个方法中重复,减少辅助代码可以让代码显得更加整洁,易于维护。
6、优化判空
判空是为让代码具有一定的健壮性,只是这样的判空代码多了,会干扰阅读代码的流畅性。
原始代码:
String isocode = user.getAddress(). getCountry(). getIsocode(). toUpperCase();
```java
因为任何访问对象方法或属性的调用都可能会导致NPE,因此如果要确保不触发异常,就得在访问每一个值之前对其进行明确的检查:
if (user != null) {
Address address = user.getAddress();
if (address != null) {
Country country = address.getCountry();
if (country != null) {
String isocode = country.getIsocode();
if (isocode != null) {
isocode = isocode.toUpperCase();
}
}
}
}
**Java8 - Optional类可以用Optional类代替冗长的null检查:**
```java
String isocode = Optional.ofNullable(user)
.flatMap(User::getAddress)
.flatMap(Address::getCountry)
.map(Country::getIsocode)
.orElse("default");
这种写法就显得很简洁,简洁是一种美。
7、组合函数模式
1、是一个非常容易理解上手、实用、对代码可读性和可维护性起到很有效的编程原则。
2、组合的方式是在代码重构中使用比较多,在重构代码之前需要读懂代码做了哪几件事,再进行重构。组合函数要求所有的公有方法的读起来像一系列执行步骤的概要,而这些步骤的真正实现细节是在私有方法里面的。
3、组合函数要求将一个大函数拆成多个子函数的组合。
8、SLAP - 抽象层次一致性
抽象层次一致性要求方法体的内容必须在同一个抽象层次上。如果高层次抽象和底层细节杂糅在一起,就会显得很乱,难以理解、
9、switch语句
10、方法参数
从复杂度的角度看,方法的参数个数越少方法的复杂度相对越低
11、三元函数
12、参数对象
13、动词和关键字
14、抽离try/catch代码块
原始代码:
优化:
15、错误处理就是一件事
函数应该只做一件事。错误处理就是一件事。因此,处理错误的函数不该做其他事。这意味着如何关键字try在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finally代码块后面也不该有其他内容。
16、别重复自己
七、命名的艺术
1、避免误导
以数字系列命名(a1,a2…aN)是依义命名的对立面。这样的名称纯属误导,完全没有提供正确信息,没有提供导向作者意图的线索
2、避免废话
废话是另一种意义的区分。假设你有一个Product类。如果还有一个ProductInfo或ProductData类,那他们的名称虽然不同,但是表达的含义没什么区别。Info和Data就像a,an和the一样是意义含混的废话。
3、使用读得出来的名称
4、使用可搜索的名称
1、单字母名称和数字常量有一个问题,就是很难在一大篇文字中找到。
5、类名
6、方法名
八、错误处理
1、使用异常而非返回码
以上代码搞乱了调用者代码,调用者必须在调用之后即刻检查错误。所以遇到错误时,最好抛出一个异常,调用代码很整洁,其逻辑不会被错误处理搞乱。
2、先写Try-Catch-Finally语句
3、别返回null值
这样就可以减少判空的代码,使得代码更加整洁优美
4、别传null值
在方法中返回null值是糟糕的做法,但是将null值传递给其他方法就更加糟糕 。除非API要求你传递null值,否则就要尽可能避免传递null值。
九、单元测试
1、保持测试整洁
2、测试带来一切好处
九、日志规约
1、应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架(SLF4J、JCL–Jakarta Commons Logging)中的API。什么是日志框架和日志系统,请参考webx作者宝宝的文章,文章里也详细说明了为什么不能直接依赖使用日志系统而是日志框架,以及应用的pom中如何做dependencyManagement。说明:日志框架(SLF4J、JCL–Jakarta Commons Logging)的使用方式(推荐使用SLF4J):使用SLF4J:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Test.class);
2、在日志输出时,字符串变量之间的拼接使用占位符的方式。说明:因为String字符串的拼接会使用StringBuilder的append()方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。正例:
logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
3、生产环境禁止直接使用System.out 或System.err 输出日志或使用e.printStackTrace()打印异常堆栈。说明:标准日志输出与标准错误输出文件每次Jboss重启时才滚动,如果大量输出送往这两个文件,容易造成文件大小超过操作系统大小限制。
4、异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么往上抛。正例:logger.error(“inputParams:{} and errorMessage:{}”, 各类参数或者对象toString(), e.getMessage());
5、日志打印时禁止直接用JSON工具将对象转换成String。说明:如果对象里某些get方法被覆写,存在抛出异常的情况,则可能会因为打印日志而影响正常业务流程的执行。正例:打印日志时仅打印出业务相关属性值或者调用其对象的toString()方法。
6、谨慎地记录日志。生产环境禁止输出debug日志;有选择地输出info日志;如果使用warn来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
十、异常处理
1、catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。说明:对大段代码进行try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。
2、不要在finally块中使用return。说明:try块中的return语句执行成功后,并不马上返回,而是继续执行finally块中的语句,如果此处存在return语句,则在此直接返回,无情丢弃掉try块中的返回点
private int x = 0;
public int checkReturn() {
try {
// x等于1,此处不返回
return ++x;
} finally {
// 返回的结果是2
return ++x;
}
}