本文主要记载

  • 1 命名多长最合适?
  • 2 利用上下文简化命名
  • 3 命名要可读、可搜索
  • 4 如何命名接口和抽象类?
  • 5 注释到底该写什么?
  • 6 注释是不是越多越好?
  • 7 类、函数多大才合适?
  • 8 一行代码多长最合适?
  • 9 善用空行分割单元块
  • 10 四格缩进还是两格缩进?
  • 11 大括号是否要另起一行?
  • 12 类中成员的排列顺序
  • 13 把代码分割成更小的单元块
  • 14 避免函数参数过多
  • 15 勿用函数参数来控制逻辑
  • 16 函数设计要职责单一
  • 17 移除过深的嵌套层次
  • 18 常量取代魔法数字
  • 19 使用解释性变量来解释复杂表达式
  • 20 统一编码规范

编码规范大部分都简单明了,在代码细节方面,能立竿见影地改善质量。

命名

大到项目名、模块名、包名、对外暴露的接口,小到类名、函数名、变量名、参数名,只要是做开发,我们就逃不过“起名字”这一关。
命名的好坏,对于代码的可读性来说非常重要,甚至可以说是起决定性作用的。
除此之外,命名能力也体现了一个程序员的基本编程素养。

那具体应该怎么命名呢?好的命名有啥标准吗?

1 命名多长最合适?

有的人喜欢很长的命名方式,觉得命名一定要准确达意,哪怕长一点也没关系,所以,这类同事的项目里,类名、函数名都很长。
有的人喜欢短的命名方式,能用缩写就尽量用缩写,所以,项目里到处都是包含各种缩写的命名。
这两种命名方式,哪种更值得推荐呢?

实际上,在足够表达其含义的情况下,命名当然是越短越好。但是,大部分情况下,短的命名都没有长的命名更能达意。
可以使用大家都熟知的缩写,比如,sec 表示 secondstr 表示 stringnum 表示 numberdoc 表示 document
对于作用域比较小的变量,我们可以使用相对短的命名,比如一些函数内的临时变量。
相反,对于类名这种作用域比较大的,我更推荐用长的命名方式。

总之,命名的一个原则就是可以能够准确达意思为目标。
命名的时候,我们一定要学会换位思考,假设自己不熟悉这块代码,从代码阅读者的角度去考量命名是否足够直观。

2 利用上下文简化命名

看一个简单的例子。

1
2
3
4
5
6
public class User {
private String userName;
private String userPassword;
private String userAvatarUrl;
//...
}

User 类这样一个上下文中,我们没有在成员变量的命名中重复添加 “user” 这样一个前缀单词,而是直接命名为 namepasswordavatarUrl
在使用这些属性时候,我们能借助对象这样一个上下文,表意也足够明确。
具体代码如下所示:

1
2
User user = new User();
user.getName(); // 借助user对象这个上下文

除了类之外,函数参数也可以借助函数这个上下文来简化命名。

1
2
3
public void uploadUserAvatarImageToAliyun(String userAvatarImageUri);
//利用上下文简化为:
public void uploadUserAvatarImageToAliyun(String imageUri);

3 命名要可读、可搜索

这里所说的 “可读” ,指的是不要用一些特别生僻、难发音的英文单词来命名。

这个单词最好是简单的,你不一定知道它表示什么意思,但基本上都能读得上来,不影响沟通交流,这就算是一个比较好的命名。

我们在命名的时候,统一规约是很重要的,能减少很多不必要的麻烦。
大家都用 “selectXXX” 表示查询,你就不要用 “queryXXX” ;大家都用 “insertXXX” 表示插入一条数据,你就要不用 “addXXX”

4 如何命名接口和抽象类?

对于接口的命名,一般有两种比较常见的方式。一种是加前缀 “I” ,表示一个 Interface 。比如 IUserService ,对应的实现类命名为 UserService 。另一种是不加前缀,比如 UserService ,对应的实现类加后缀 “Impl” ,比如 UserServiceImpl

对于抽象类的命名,也有两种方式,一种是带上前缀 “Abstract”,比如 AbstractConfiguration;另一种是不带前缀 “Abstract”。实际上,对于接口和抽象类,选择哪种命名方式都是可以的,只要项目里能够统一就行。

注释

命名很重要,注释跟命名同等重要。
很多书籍认为,好的命名完全可以替代注释。如果需要注释,那说明命名不够好,需要在命名上下功夫,而不是添加注释。
但是命名再好,毕竟有长度限制,不可能足够详尽,而这个时候,注释就是一个很好的补充。

5 注释到底该写什么?

注释的目的就是让代码更容易看懂。只要符合这个要求的内容,你就可以将它写到注释里。
总结一下,注释的内容主要包含这样三个方面:做什么(what)、为什么(why)、怎么做(how)

对于一些复杂的类和接口,我们可能还需要写明 “如何用” 。

6 注释是不是越多越好?

注释太多和太少都有问题。
太多,有可能意味着代码写得不够可读,需要写很多注释来补充。除此之外,注释太多也会对代码本身的阅读起到干扰。而且,后期的维护成本也比较高,有时候代码改了,注释忘了同步修改,就会让代码阅读者更加迷惑。
如果代码中一行注释都没有,那只能说明这个程序员很懒,我们要适当督促一下,让他注意添加一些必要的注释。

类和函数一定要写注释,而且要写得尽可能全面、详细,而函数内部的注释要相对少一些,一般都是靠好的命名、提炼函数、解释性变量、总结性注释来提高代码的可读性。

代码风格

7 类、函数多大才合适?

总体上来讲,类或函数的代码行数不能太多,但也不能太少。
类或函数的代码行数太多,一个类上千行,一个函数几百行,逻辑过于繁杂,阅读代码的时候,很容易就会看了后面忘了前面。
相反,类或函数的代码行数太少,在代码总量相同的情况下,被分割成的类和函数就会相应增多,调用关系就会变得更复杂,阅读某个代码逻辑的时候,需要频繁地在 n 多类或者 n 多函数之间跳来跳去,阅读体验也不好。

对于函数代码行数的最大限制,网上有一种说法,那就是不要超过一个显示屏的垂直高度。
比如,在我的电脑上,如果要让一个函数的代码完整地显示在 IDE 中,那最大代码行数不能超过 50
因为超过一屏之后,在阅读代码的时候,为了串联前后的代码逻辑,就可能需要频繁地上下滚动屏幕,阅读体验不好不说,还容易出错。

8 一行代码多长最合适?

Google Java Style Guide 文档中,一行代码最长限制为 100 个字符。
不过,不同的编程语言、不同的规范、不同的项目团队,对此的限制可能都不相同。
不管这个限制是多少,总体上来讲要遵循的一个原则是:一行代码最长不能超过 IDE 显示的宽度。需要滚动鼠标才能查看一行的全部代码,显然不利于代码的阅读。当然,这个限制也不能太小,太小会导致很多稍长点的语句被折成两行,也会影响到代码的整洁,不利于阅读。

9 善用空行分割单元块

对于比较长的函数,如果逻辑上可以分为几个独立的代码块,在不方便将这些独立的代码块抽取成小函数的情况下,为了让逻辑更加清晰,除了之前提到的用注释的方法之外,我们还可以使用空行来分割各个代码块。

除此之外,在类的成员变量与函数之间、静态成员变量与普通成员变量之间、各函数之间、甚至各成员变量之间,我们都可以通过添加空行的方式,让这些不同模块的代码之间,界限更加明确。写代码就类似写文章,善于应用空行,可以让代码的整体结构看起来更加有清晰、有条理。

10 四格缩进还是两格缩进?

11 大括号是否要另起一行?

12 类中成员的排列顺序

因为是代码风格,所以每个人意见都是不同的,只要团队统一、自己看着舒服就好,项目里添加统一的 format 规则,格式化后显示相同代码
然后有一个叫 Alibaba Java Coding Guidelines 的插件,可以按照插件中的要求编码。

编程技巧

13 把代码分割成更小的单元块

大部分人阅读代码的习惯都是,先看整体再看细节。
所以,我们要有模块化和抽象思维,善于将大块的复杂逻辑提炼成类或者函数,屏蔽掉细节,让阅读代码的人不至于迷失在细节中,这样能极大地提高代码的可读性。
不过,只有代码逻辑比较复杂的时候,我们其实才建议提炼类或者函数。毕竟如果提炼出的函数只包含两三行代码,在阅读代码的时候,还得跳过去看一下,这样反倒增加了阅读成本。

举个栗子🌰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 重构前的代码
public void invest(long userId, long financialProductId) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.DATE, (calendar.get(Calendar.DATE) + 1));
if (calendar.get(Calendar.DAY_OF_MONTH) == 1) {
return;
}
//...
}

// 重构后的代码:提炼函数之后逻辑更加清晰
public void invest(long userId, long financialProductId) {
if (isLastDayOfMonth(new Date())) {
return;
}
//...
}

public boolean isLastDayOfMonth(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.DATE, (calendar.get(Calendar.DATE) + 1));
if (calendar.get(Calendar.DAY_OF_MONTH) == 1) {
return true;
}
return false;
}

这段代码重构之后,我们将这部分逻辑抽象成一个函数,并且命名为 isLastDayOfMonth,从名字就能清晰地了解它的功能,判断今天是不是当月的最后一天。这里,我们就是通过将复杂的逻辑代码提炼成函数,大大提高了代码的可读性。

14 避免函数参数过多

参数过多,会影响到代码的可读性,使用起来也不方便。针对参数过多的情况,一般有 2 种处理方法。

  • 考虑函数是否职责单一,是否能通过拆分成多个函数的方式来减少参数。示例代码如下所示:

    1
    2
    3
    4
    5
    6
    public User getUser(String username, String telephone, String email);

    // 拆分成多个函数
    public User getUserByUsername(String username);
    public User getUserByTelephone(String telephone);
    public User getUserByEmail(String email);
  • 将函数的参数封装成对象。

    如果函数是对外暴露的远程接口,将参数封装成对象,还可以提高接口的兼容性。在往接口中添加新的参数的时候,老的远程接口调用者有可能就不需要修改代码来兼容新的接口了。

15 勿用函数参数来控制逻辑

不要在函数中使用布尔类型的标识参数来控制内部逻辑,true 的时候走这块逻辑,false 的时候走另一块逻辑。
这明显违背了单一职责原则和接口隔离原则。
可以将其拆成两个函数,可读性上也要更好。

1
2
3
4
5
public void buyCourse(long userId, long courseId, boolean isVip);

// 将其拆分成两个函数
public void buyCourse(long userId, long courseId);
public void buyCourseForVip(long userId, long courseId);

还有一种 “根据参数是否为 null” 来控制逻辑的情况。
针对这种情况,我们也应该将其拆分成多个函数。拆分之后的函数职责更明确,不容易用错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public List<Transaction> selectTransactions(Long userId, Date startDate, Date endDate) {
if (startDate != null && endDate != null) {
// 查询两个时间区间的transactions
}
if (startDate != null && endDate == null) {
// 查询startDate之后的所有transactions
}
if (startDate == null && endDate != null) {
// 查询endDate之前的所有transactions
}
if (startDate == null && endDate == null) {
// 查询所有的transactions
}
}

// 拆分成多个public函数,更加清晰、易用
public List<Transaction> selectTransactionsBetween(Long userId, Date startDate, Date endDate) {
return selectTransactions(userId, startDate, endDate);
}

public List<Transaction> selectTransactionsStartWith(Long userId, Date startDate) {
return selectTransactions(userId, startDate, null);
}

public List<Transaction> selectTransactionsEndWith(Long userId, Date endDate) {
return selectTransactions(userId, null, endDate);
}

public List<Transaction> selectAllTransactions(Long userId) {
return selectTransactions(userId, null, null);
}

private List<Transaction> selectTransactions(Long userId, Date startDate, Date endDate) {
// ...
}

16 函数设计要职责单一

单一职责原则针对的是类、模块这样的应用对象。
实际上,对于函数的设计来说,更要满足单一职责原则。相对于类和模块,函数的粒度比较小,代码行数少,所以在应用单一职责原则的时候,没有像应用到类或者模块那样模棱两可,能多单一就多单一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public boolean checkUserIfExisting(String telephone, String username, String email)  { 
if (!StringUtils.isBlank(telephone)) {
User user = userRepo.selectUserByTelephone(telephone);
return user != null;
}

if (!StringUtils.isBlank(username)) {
User user = userRepo.selectUserByUsername(username);
return user != null;
}

if (!StringUtils.isBlank(email)) {
User user = userRepo.selectUserByEmail(email);
return user != null;
}

return false;
}

// 拆分成三个函数
public boolean checkUserIfExistingByTelephone(String telephone);
public boolean checkUserIfExistingByUsername(String username);
public boolean checkUserIfExistingByEmail(String email);

17 移除过深的嵌套层次

  • 去掉多余的 ifelse 语句。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 示例一
    public double caculateTotalAmount(List<Order> orders) {
    if (orders == null || orders.isEmpty()) {
    return 0.0;
    } else { // 此处的else可以去掉
    double amount = 0.0;
    // ...
    return amount;
    }
    }

    // 示例二
    public List<String> matchStrings(List<String> strList,String substr) {
    List<String> matchedStrings = new ArrayList<>();
    if (strList != null && substr != null) {
    for (String str : strList) {
    if (str != null) { // 跟下面的if语句可以合并在一起
    if (str.contains(substr)) {
    // ...
    }
    }
    }
    }
    return matchedStrings;
    }
  • 使用编程语言提供的 continuebreakreturn 关键字,提前退出嵌套。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 重构后的代码:使用continue提前退出
    public List<String> matchStrings(List<String> strList,String substr) {
    List<String> matchedStrings = new ArrayList<>();
    if (strList != null && substr != null){
    for (String str : strList) {
    if (str == null || !str.contains(substr)) {
    continue;
    }
    matchedStrings.add(str);
    // ...
    }
    }
    return matchedStrings;
    }
  • 调整执行顺序来减少嵌套。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 重构后的代码:使用continue提前退出
    public List<String> matchStrings(List<String> strList,String substr) {
    if (strList == null || substr == null) {//先判空
    return Collections.emptyList();
    }
    List<String> matchedStrings = new ArrayList<>();
    for (String str : strList) {
    if (str == null || !str.contains(substr)) {
    continue;
    }
    matchedStrings.add(str);
    // ...
    }
    return matchedStrings;
    }
  • 将部分嵌套逻辑封装成函数调用,以此来减少嵌套。

  • 常用的还有通过使用多态来替代 if-else、switch-case 条件判断的方法。

18 常量取代魔法数字

1
2
3
4
5
6
7
8
9
public double CalculateCircularArea(double radius) {
return (3.1415) * radius * radius;
}

// 常量替代魔法数字
public static final Double PI = 3.1415;
public double CalculateCircularArea(double radius) {
return PI * radius * radius;
}

19 使用解释性变量来解释复杂表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
if (date.after(SUMMER_START) && date.before(SUMMER_END)) {
// ...
} else {
// ...
}

// 引入解释性变量后逻辑更加清晰
boolean isSummer = date.after(SUMMER_START)&&date.before(SUMMER_END);
if (isSummer) {
// ...
} else {
// ...
}

20 统一编码规范

最后,还有一条非常重要的,那就是,项目、团队,甚至公司,一定要制定统一的编码规范,并且通过 Code Review 督促执行,这对提高代码质量有立竿见影的效果。