<audio id="audio" title="33 | 理论五：让你最快速地改善代码质量的20条编程规范（下）" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3d/f5/3d70a002608ebd837fe6dc3c6f899ff5.mp3"></audio>

上两节课，我们讲了命名和注释、代码风格，今天我们来讲一些比较实用的编程技巧，帮你切实地提高代码可读性。这部分技巧比较琐碎，也很难罗列全面，我仅仅总结了一些我认为比较关键的，更多的技巧需要你在实践中自己慢慢总结、积累。

话不多说，让我们正式开始今天的学习吧！

## 1.把代码分割成更小的单元块

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

这里我举一个例子来进一步解释一下。代码具体如下所示。重构前，在invest()函数中，最开始的那段关于时间处理的代码，是不是很难看懂？重构之后，我们将这部分逻辑抽象成一个函数，并且命名为isLastDayOfMonth，从名字就能清晰地了解它的功能，判断今天是不是当月的最后一天。这里，我们就是通过将复杂的逻辑代码提炼成函数，大大提高了代码的可读性。

```
// 重构前的代码
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;
}

```

## 2.避免函数参数过多

我个人觉得，函数包含3、4个参数的时候还是能接受的，大于等于5个的时候，我们就觉得参数有点过多了，会影响到代码的可读性，使用起来也不方便。针对参数过多的情况，一般有2种处理方法。

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

```
public User getUser(String username, String telephone, String email);

// 拆分成多个函数
public User getUserByUsername(String username);
public User getUserByTelephone(String telephone);
public User getUserByEmail(String email);

```

- 将函数的参数封装成对象。示例代码如下所示：

```
public void postBlog(String title, String summary, String keywords, String content, String category, long authorId);

// 将参数封装成对象
public class Blog {
  private String title;
  private String summary;
  private String keywords;
  private Strint content;
  private String category;
  private long authorId;
}
public void postBlog(Blog blog);

```

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

## 3.勿用函数参数来控制逻辑

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

```
public void buyCourse(long userId, long courseId, boolean isVip);

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

```

不过，如果函数是private私有函数，影响范围有限，或者拆分之后的两个函数经常同时被调用，我们可以酌情考虑保留标识参数。示例代码如下所示：

```
// 拆分成两个函数的调用方式
boolean isVip = false;
//...省略其他逻辑...
if (isVip) {
  buyCourseForVip(userId, courseId);
} else {
  buyCourse(userId, courseId);
}

// 保留标识参数的调用方式更加简洁
boolean isVip = false;
//...省略其他逻辑...
buyCourse(userId, courseId, isVip);

```

除了布尔类型作为标识参数来控制逻辑的情况外，还有一种“根据参数是否为null”来控制逻辑的情况。针对这种情况，我们也应该将其拆分成多个函数。拆分之后的函数职责更明确，不容易用错。具体代码示例如下所示：

```
public List&lt;Transaction&gt; selectTransactions(Long userId, Date startDate, Date endDate) {
  if (startDate != null &amp;&amp; endDate != null) {
    // 查询两个时间区间的transactions
  }
  if (startDate != null &amp;&amp; endDate == null) {
    // 查询startDate之后的所有transactions
  }
  if (startDate == null &amp;&amp; endDate != null) {
    // 查询endDate之前的所有transactions
  }
  if (startDate == null &amp;&amp; endDate == null) {
    // 查询所有的transactions
  }
}

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

public List&lt;Transaction&gt; selectTransactionsStartWith(Long userId, Date startDate) {
  return selectTransactions(userId, startDate, null);
}

public List&lt;Transaction&gt; selectTransactionsEndWith(Long userId, Date endDate) {
  return selectTransactions(userId, null, endDate);
}

public List&lt;Transaction&gt; selectAllTransactions(Long userId) {
  return selectTransactions(userId, null, null);
}

private List&lt;Transaction&gt; selectTransactions(Long userId, Date startDate, Date endDate) {
  // ...
}

```

## 4.函数设计要职责单一

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

具体的代码示例如下所示：

```
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);

```

## 5.移除过深的嵌套层次

代码嵌套层次过深往往是因为if-else、switch-case、for循环过度嵌套导致的。我个人建议，嵌套最好不超过两层，超过两层之后就要思考一下是否可以减少嵌套。过深的嵌套本身理解起来就比较费劲，除此之外，嵌套过深很容易因为代码多次缩进，导致嵌套内部的语句超过一行的长度而折成两行，影响代码的整洁。

解决嵌套过深的方法也比较成熟，有下面4种常见的思路。

- 去掉多余的if或else语句。代码示例如下所示：

```
// 示例一
public double caculateTotalAmount(List&lt;Order&gt; orders) {
  if (orders == null || orders.isEmpty()) {
    return 0.0;
  } else { // 此处的else可以去掉
    double amount = 0.0;
    for (Order order : orders) {
      if (order != null) {
        amount += (order.getCount() * order.getPrice());
      }
    }
    return amount;
  }
}

// 示例二
public List&lt;String&gt; matchStrings(List&lt;String&gt; strList,String substr) {
  List&lt;String&gt; matchedStrings = new ArrayList&lt;&gt;();
  if (strList != null &amp;&amp; substr != null) {
    for (String str : strList) {
      if (str != null) { // 跟下面的if语句可以合并在一起
        if (str.contains(substr)) {
          matchedStrings.add(str);
        }
      }
    }
  }
  return matchedStrings;
}

```

- 使用编程语言提供的continue、break、return关键字，提前退出嵌套。代码示例如下所示：

```
// 重构前的代码
public List&lt;String&gt; matchStrings(List&lt;String&gt; strList,String substr) {
  List&lt;String&gt; matchedStrings = new ArrayList&lt;&gt;();
  if (strList != null &amp;&amp; substr != null){ 
    for (String str : strList) {
      if (str != null &amp;&amp; str.contains(substr)) {
        matchedStrings.add(str);
        // 此处还有10行代码...
      }
    }
  }
  return matchedStrings;
}

// 重构后的代码：使用continue提前退出
public List&lt;String&gt; matchStrings(List&lt;String&gt; strList,String substr) {
  List&lt;String&gt; matchedStrings = new ArrayList&lt;&gt;();
  if (strList != null &amp;&amp; substr != null){ 
    for (String str : strList) {
      if (str == null || !str.contains(substr)) {
        continue; 
      }
      matchedStrings.add(str);
      // 此处还有10行代码...
    }
  }
  return matchedStrings;
}

```

- 调整执行顺序来减少嵌套。具体的代码示例如下所示：

```
// 重构前的代码
public List&lt;String&gt; matchStrings(List&lt;String&gt; strList,String substr) {
  List&lt;String&gt; matchedStrings = new ArrayList&lt;&gt;();
  if (strList != null &amp;&amp; substr != null) {
    for (String str : strList) {
      if (str != null) {
        if (str.contains(substr)) {
          matchedStrings.add(str);
        }
      }
    }
  }
  return matchedStrings;
}

// 重构后的代码：先执行判空逻辑，再执行正常逻辑
public List&lt;String&gt; matchStrings(List&lt;String&gt; strList,String substr) {
  if (strList == null || substr == null) { //先判空
    return Collections.emptyList();
  }

  List&lt;String&gt; matchedStrings = new ArrayList&lt;&gt;();
  for (String str : strList) {
    if (str != null) {
      if (str.contains(substr)) {
        matchedStrings.add(str);
      }
    }
  }
  return matchedStrings;
}

```

- 将部分嵌套逻辑封装成函数调用，以此来减少嵌套。具体的代码示例如下所示：

```
// 重构前的代码
public List&lt;String&gt; appendSalts(List&lt;String&gt; passwords) {
  if (passwords == null || passwords.isEmpty()) {
    return Collections.emptyList();
  }
  
  List&lt;String&gt; passwordsWithSalt = new ArrayList&lt;&gt;();
  for (String password : passwords) {
    if (password == null) {
      continue;
    }
    if (password.length() &lt; 8) {
      // ...
    } else {
      // ...
    }
  }
  return passwordsWithSalt;
}

// 重构后的代码：将部分逻辑抽成函数
public List&lt;String&gt; appendSalts(List&lt;String&gt; passwords) {
  if (passwords == null || passwords.isEmpty()) {
    return Collections.emptyList();
  }

  List&lt;String&gt; passwordsWithSalt = new ArrayList&lt;&gt;();
  for (String password : passwords) {
    if (password == null) {
      continue;
    }
    passwordsWithSalt.add(appendSalt(password));
  }
  return passwordsWithSalt;
}

private String appendSalt(String password) {
  String passwordWithSalt = password;
  if (password.length() &lt; 8) {
    // ...
  } else {
    // ...
  }
  return passwordWithSalt;
}

```

除此之外，常用的还有通过使用多态来替代if-else、switch-case条件判断的方法。这个思路涉及代码结构的改动，我们会在后面的章节中讲到，这里就暂时不展开说明了。

## 6.学会使用解释性变量

常用的用解释性变量来提高代码的可读性的情况有下面2种。

- 常量取代魔法数字。示例代码如下所示：

```
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;
}

```

- 使用解释性变量来解释复杂表达式。示例代码如下所示：

```
if (date.after(SUMMER_START) &amp;&amp; date.before(SUMMER_END)) {
  // ...
} else {
  // ...
}

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

```

## 重点回顾

好了，今天的内容到此就讲完了。除了今天讲的编程技巧，前两节课我们还分别讲解了命名与注释、代码风格。现在，我们一块来回顾复习一下这三节课的重点内容。

**1.关于命名**

- 命名的关键是能准确达意。对于不同作用域的命名，我们可以适当地选择不同的长度。
- 我们可以借助类的信息来简化属性、函数的命名，利用函数的信息来简化函数参数的命名。
- 命名要可读、可搜索。不要使用生僻的、不好读的英文单词来命名。命名要符合项目的统一规范，也不要用些反直觉的命名。
- 接口有两种命名方式：一种是在接口中带前缀“I”；另一种是在接口的实现类中带后缀“Impl”。对于抽象类的命名，也有两种方式，一种是带上前缀“Abstract”，一种是不带前缀。这两种命名方式都可以，关键是要在项目中统一。

**2.关于注释**

- 注释的内容主要包含这样三个方面：做什么、为什么、怎么做。对于一些复杂的类和接口，我们可能还需要写明“如何用”。
- 类和函数一定要写注释，而且要写得尽可能全面详细。函数内部的注释要相对少一些，一般都是靠好的命名、提炼函数、解释性变量、总结性注释来提高代码可读性。

**3.关于代码风格**

- 函数、类多大才合适？函数的代码行数不要超过一屏幕的大小，比如50行。类的大小限制比较难确定。
- 一行代码多长最合适？最好不要超过IDE的显示宽度。当然，也不能太小，否则会导致很多稍微长点的语句被折成两行，也会影响到代码的整洁，不利于阅读。
- 善用空行分割单元块。对于比较长的函数，为了让逻辑更加清晰，可以使用空行来分割各个代码块。
- 四格缩进还是两格缩进？我个人比较推荐使用两格缩进，这样可以节省空间，尤其是在代码嵌套层次比较深的情况下。不管是用两格缩进还是四格缩进，一定不要用tab键缩进。
- 大括号是否要另起一行？将大括号放到跟上一条语句同一行，可以节省代码行数。但是将大括号另起新的一行的方式，左右括号可以垂直对齐，哪些代码属于哪一个代码块，更加一目了然。
- 类中成员怎么排列？在Google Java编程规范中，依赖类按照字母序从小到大排列。类中先写成员变量后写函数。成员变量之间或函数之间，先写静态成员变量或函数，后写普通变量或函数，并且按照作用域大小依次排列。

**4.关于编码技巧**

- 将复杂的逻辑提炼拆分成函数和类。
- 通过拆分成多个函数或将参数封装为对象的方式，来处理参数过多的情况。
- 函数中不要使用参数来做代码执行逻辑的控制。
- 函数设计要职责单一。
- 移除过深的嵌套层次，方法包括：去掉多余的if或else语句，使用continue、break、return关键字提前退出嵌套，调整执行顺序来减少嵌套，将部分嵌套逻辑抽象成函数。
- 用字面常量取代魔法数。
- 用解释性变量来解释复杂表达式，以此提高代码可读性。

**5.统一编码规范**

除了这三节讲到的比较细节的知识点之外，最后，还有一条非常重要的，那就是，项目、团队，甚至公司，一定要制定统一的编码规范，并且通过Code Review督促执行，这对提高代码质量有立竿见影的效果。

## 课堂讨论

到此为止，我们整个20条编码规范就讲完了。不知道你掌握了多少呢？除了今天我提到的这些，还有哪些其他的编程技巧，可以明显改善代码的可读性？

试着在留言区总结罗列一下，和同学一起交流和分享。如果有收获，也欢迎你把这篇文章分享给你的朋友。
