CategoryResourceRepost/极客时间专栏/设计模式之美/设计原则与思想:规范与重构/33 | 理论五:让你最快速地改善代码质量的20条编程规范(下).md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

434 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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条编码规范就讲完了。不知道你掌握了多少呢除了今天我提到的这些还有哪些其他的编程技巧可以明显改善代码的可读性
试着在留言区总结罗列一下,和同学一起交流和分享。如果有收获,也欢迎你把这篇文章分享给你的朋友。