前言
最近一段时间进行了部分代码的code review。其中有 review 的,也有被review的。在这过程中发现了许多问题,而其中就包含许多本不该发生的。同样的,这些不该发生的问题如果携带上线,随之而来的则是一个个的生产事故。对于金融系统来说,会直接造成资产损失,而对于医疗软件而言,甚至涉及生命安全。经过分析分类后,以下几点是最为常见的问题:
代码顺序倒置
代码顺序,这一点太容易忽略了,以至于有时让我们觉得这根本不是问题。话不多说,我们先来看例子:
反例:
public void createUser(String telephone, String name, Integer age){
//1. 校验手机号
if (StringUitls.isBlank(telephone)) {
...
}
//2. 查询用户
User uesr = queryUser(telephone);
//3. 校验name ...
if (StringUitls.isBlank(name)) {
...
}
//4. 校验年龄
if (null == age) {
...
}
//5. 保存用户
saveUser(telephone, name, age);
}
上述代码中存在若干问题,有以下几点:
- 代码顺序混乱,其中 查询用户 不应该在第二步,为什么呢?因为如果 name 为空 或 age 为空时,则会白白浪费一次数据库查询,而这次查询,原本是不该发生的。
- 校验逻辑和核心逻辑混在一起,造成红花与绿叶不分。
正例:
public void createUser(String telephone, String name, Integer age) {
//检查参数
checkCreateUserParams(telephone, name, age)
// 保存用户
saveUser(telephone, name, age);
}
//校验参数
private void checkCreateUserParams(String telephone, String name, Integer age) {
if (StringUitls.isBlank(telephone)) {
...
}
if (StringUitls.isBlank(name)) {
...
}
if (null == age) {
...
}
}
//保存用户
private void saveUser(String telephone, String name, Integer age) {
User uesr = queryUser(telephone);
if (user == null) {
saveUser(telephone, name, age);
}
}
建议:实际上,工作中编写的逻辑会比上述复杂很多,但道理是相通的。代码合理有效顺序,也会带来诸多好处,例如:减少IO操作(调用DAO方法),较少数据传输(调用RPC方法)。而合适的顺序应该遵循:参数校验 优先于 核心流程处理。
无意的 NullPointerException
我们都知道 NPE (NullPointerException) 的判断是调用者的职责。而在方法实现中,有很多同学稍微不注意,就会导致一些低级的 NPE (NullPointerException) 产生。例如:
反例:
public void processUserInfo(String telephone) {
User uesr = queryUser(telephone);
if (user != null) {
...
}
Long uerId = user.getOid;
}
问题:
此时当 telephone 查询的用户不存在时,则会产生 NullPointerException,你还别笑,这类问题发生的频率还不低,非常值得我们注意。
正例:
public void processUserInfo(String telephone) {
User uesr = queryUser(telephone);
if (user == null) {
return;
}
...
}
职责模糊
在学习设计模式时,第一条设计原则就是:单一职责。这给我们的启示是:在定义方法时,应该尽量原子,让其承担单一职责。但在实践过程中,则出现许多比较怪的实现,以下面代码为例:
反例:
//1. 调用创建用户
public void invokerCreateUser(String telephone, String name, Integer age) {
if (StringUitls.isBlank(telephone)) {
...
}
if (StringUitls.isBlank(name)) {
...
}
if (null == age) {
...
}
createUser()
}
//2. 创建用户原子方法
public void createUser(String telephone, String name, Integer age) {
User uesr = queryUser(telephone);
if (user == null) {
saveUser(telephone, name, age);
}
}
- invokerCreateUser 为创建用户调用方。
- createUser 为创建用户提供方。
问题:
- invokerCreateUser 作为 调用方承担了 本该 createUser 提供方承担的职责。
建议:
无论在什么场景下,方法的提供方都需要负责进行参数的校验,而不是将这些职责委托到调用方。否则诸多调用方都需要进行参数验证,代码坏味道随即而来!同样的道理,思维发散开来。我们在定义方法时:属于该方法的职责,我们一个也不能少的承担,不属于该方法的职责,一个也不需要承担。
方法过长
抽取方法这点非常重要,好处包括但不限于以下几点:
- 方法越小,职责越单一,其复用性越高,从而减少重复代码的产生。
- 代码逻辑清晰,可以使主干代码更清晰,易于维护。
…
有很多同学,习惯在一个方法中编写成百上千行的代码彰显自己的能力。但事实上这样的代码及其不易维护,且阅读都非常吃劲。那多少行才是最合适的呢?这一点,在阿里巴巴的《Java手册》中提到:
推荐:单个方法的总行数不超过 80 行。(除注释之外的方法签名、 左右大括号、方法内代码、空行、回车及任何不可见字符的总行数不超过 80 行。)
同样也给出了这样做的原因:代码逻辑分清红花和绿叶,个性和共性,绿叶逻辑单独出来成为额外方法,使主干代码更加清晰;共性逻辑抽取成为共性方法,便于复用和维护。
以下述方法为例:
反例:
public void createUser(String telephone, String name, Integer age){
//
if (StringUitls.isBlank(telephone)) {
...
}
if (StringUitls.isBlank(name)) {
...
}
if (null == age) {
...
}
//用户不存在则保存
User uesr = queryUser(telephone);
if (user == null) {
saveUser(telephone, name, age);
}
}
正例:
public void createUser(String telephone, String name, Integer age) {
//检查参数
checkCreateUserParams(telephone, name, age)
// 保存用户
saveUser(telephone, name, age);
}
//校验参数
private void checkCreateUserParams(String telephone, String name, Integer age) {
if (StringUitls.isBlank(telephone)) {
...
}
if (StringUitls.isBlank(name)) {
...
}
if (null == age) {
...
}
}
//保存用户
private void saveUser(String telephone, String name, Integer age) {
User uesr = queryUser(telephone);
if (user == null) {
saveUser(telephone, name, age);
}
}
无用代码引用
去除无用代码,其实有很多方面都是需要注意的,其中包括但不限于以下几点:
- 去除未使用的 import。(window 可用快捷键:Ctrl+Alt + O 进行格式化)。
- 去除未使用的局部变量。
反例:
private void checkCreateUserParams(String telephone, String name, Integer age) {
//1. 未使用的局部变量 sex
Integer sex = "";
if (StringUitls.isBlank(telephone)) {
...
}
if (StringUitls.isBlank(name)) {
...
}
if (null == age) {
...
}
}
正例:
private void checkCreateUserParams(String telephone, String name, Integer age) {
if (StringUitls.isBlank(telephone)) {
...
}
if (StringUitls.isBlank(name)) {
...
}
if (null == age) {
...
}
}
3. 移除未使用的方法
反例:
private void checkCreateUserParams(String telephone, String name, Integer age) {
if (StringUitls.isBlank(telephone)) {
...
}
if (StringUitls.isBlank(name)) {
...
}
if (null == age) {
...
}
}
// 未使用的私有方法
private void check(){
return ;
}
正例:
private void checkCreateUserParams(String telephone, String name, Integer age) {
if (StringUitls.isBlank(telephone)) {
...
}
if (StringUitls.isBlank(name)) {
...
}
if (null == age) {
...
}
}
建议:
- 代码中未使用到的代码,及时删除,特别是私有方法。已经上线的RPC方法,则采取版本递进的方法进行移除。未上线且未使用的RPC方法及时删除,否则上线完后删除的风险就会非常大。
挂羊头卖狗肉
这一类问题,其实在接口定义上出现的比较多。其简单来说:就是表里不一,方法中返回对象,方法名,方法请求对象三者不统一。这样的接口提供给调用者,调用者会非常疑惑,甚至想过来怼你两句!以下述代码为例:
反例:
private User saveUserInfo(String telephone) {
return queryUser(telephone);
}
问题: 方法名为 saveUser,其字面意思是:保存用户信息,但其内部实现则为查询用户信息,明显的挂羊头卖狗肉。
正例:
private User getUser(String telephone) {
return queryUser(telephone);
}
建议:
- 定义接口时,接口返回对象,方法名,请求参数 三者语义要统一,避免上述问题产生。
- 方法名尽量能够表达一个行为,例如:get / remove / put 等,而不是用名词来代替。
小结
上述几种问题是最近 code review 过程中出现最多的几类问题以及解决办法。还有一些诸如 code style 类的问题,没有在本文中描述,不过还是建议大家一定要抽时间看看《Java手册》,每次看都有新的收获!还有就是一些提升代码质量的途径,诸如:
- 回看一周前,一月前自己写的代码,对不满意的代码进行重构。
- 参加开源项目,给开源项目提交 Pull Request,提交的代码会有多人进行code review,直至满足规范后,才会进行 merge。