上两节课,我们讲了命名和注释、代码风格,今天我们来讲一些比较实用的编程技巧,帮你切实地提高代码可读性。这部分技巧比较琐碎,也很难罗列全面,我仅仅总结了一些我认为比较关键的,更多的技巧需要你在实践中自己慢慢总结、积累。
话不多说,让我们正式开始今天的学习吧!
大部分人阅读代码的习惯都是,先看整体再看细节。所以,我们要有模块化和抽象思维,善于将大块的复杂逻辑提炼成类或者函数,屏蔽掉细节,让阅读代码的人不至于迷失在细节中,这样能极大地提高代码的可读性。不过,只有代码逻辑比较复杂的时候,我们其实才建议提炼类或者函数。毕竟如果提炼出的函数只包含两三行代码,在阅读代码的时候,还得跳过去看一下,这样反倒增加了阅读成本。
这里我举一个例子来进一步解释一下。代码具体如下所示。重构前,在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;}
我个人觉得,函数包含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);
除此之外,如果函数是对外暴露的远程接口,将参数封装成对象,还可以提高接口的兼容性。在往接口中添加新的参数的时候,老的远程接口调用者有可能就不需要修改代码来兼容新的接口了。
不要在函数中使用布尔类型的标识参数来控制内部逻辑,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<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) {// ...}
我们在前面讲到单一职责原则的时候,针对的是类、模块这样的应用对象。实际上,对于函数的设计来说,更要满足单一职责原则。相对于类和模块,函数的粒度比较小,代码行数少,所以在应用单一职责原则的时候,没有像应用到类或者模块那样模棱两可,能多单一就多单一。
具体的代码示例如下所示:
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);
代码嵌套层次过深往往是因为if-else、switch-case、for循环过度嵌套导致的。我个人建议,嵌套最好不超过两层,超过两层之后就要思考一下是否可以减少嵌套。过深的嵌套本身理解起来就比较费劲,除此之外,嵌套过深很容易因为代码多次缩进,导致嵌套内部的语句超过一行的长度而折成两行,影响代码的整洁。
解决嵌套过深的方法也比较成熟,有下面4种常见的思路。
// 示例一public double caculateTotalAmount(List<Order> 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<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)) {matchedStrings.add(str);}}}}return matchedStrings;}
// 重构前的代码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)) {matchedStrings.add(str);// 此处还有10行代码...}}}return matchedStrings;}// 重构后的代码:使用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);// 此处还有10行代码...}}return matchedStrings;}
// 重构前的代码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 (str.contains(substr)) {matchedStrings.add(str);}}}}return matchedStrings;}// 重构后的代码:先执行判空逻辑,再执行正常逻辑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) {if (str.contains(substr)) {matchedStrings.add(str);}}}return matchedStrings;}
// 重构前的代码public List<String> appendSalts(List<String> passwords) {if (passwords == null || passwords.isEmpty()) {return Collections.emptyList();}List<String> passwordsWithSalt = new ArrayList<>();for (String password : passwords) {if (password == null) {continue;}if (password.length() < 8) {// ...} else {// ...}}return passwordsWithSalt;}// 重构后的代码:将部分逻辑抽成函数public List<String> appendSalts(List<String> passwords) {if (passwords == null || passwords.isEmpty()) {return Collections.emptyList();}List<String> passwordsWithSalt = new ArrayList<>();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() < 8) {// ...} else {// ...}return passwordWithSalt;}
除此之外,常用的还有通过使用多态来替代if-else、switch-case条件判断的方法。这个思路涉及代码结构的改动,我们会在后面的章节中讲到,这里就暂时不展开说明了。
常用的用解释性变量来提高代码的可读性的情况有下面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) && date.before(SUMMER_END)) {// ...} else {// ...}// 引入解释性变量后逻辑更加清晰boolean isSummer = date.after(SUMMER_START)&&date.before(SUMMER_END);if (isSummer) {// ...} else {// ...}
好了,今天的内容到此就讲完了。除了今天讲的编程技巧,前两节课我们还分别讲解了命名与注释、代码风格。现在,我们一块来回顾复习一下这三节课的重点内容。
1.关于命名
2.关于注释
3.关于代码风格
4.关于编码技巧
5.统一编码规范
除了这三节讲到的比较细节的知识点之外,最后,还有一条非常重要的,那就是,项目、团队,甚至公司,一定要制定统一的编码规范,并且通过Code Review督促执行,这对提高代码质量有立竿见影的效果。
到此为止,我们整个20条编码规范就讲完了。不知道你掌握了多少呢?除了今天我提到的这些,还有哪些其他的编程技巧,可以明显改善代码的可读性?
试着在留言区总结罗列一下,和同学一起交流和分享。如果有收获,也欢迎你把这篇文章分享给你的朋友。