[The Beauty of Design Patterns Design Principles and Thoughts: Specification and Refactoring] 33 | Theory Five: 20 Programming Specifications that Allow You to Improve Code Quality Quickly (Part 2)

In the last two classes, we talked about naming, comments, and code style. Today we will talk about some practical programming skills to help you improve the readability of your code. This part of the skills is relatively trivial, and it is difficult to list them comprehensively. I just summarized some of the key ones that I think are more important. You need to gradually summarize and accumulate more skills in practice.

Without further ado, let's officially start today's study!

1. Divide the code into smaller unit blocks

Most people have the habit of reading code, first look at the whole and then look at the details. Therefore, we must have modular and abstract thinking, and be good at refining large blocks of complex logic into classes or functions, shielding the details, so that people who read the code will not get lost in the details, which can greatly improve the readability of the code . However, only when the code logic is more complex, we actually recommend refining classes or functions. After all, if the extracted function only contains two or three lines of code, you have to skip it when reading the code, which will increase the cost of reading.

Here I give an example to further explain. The code is as follows. Before refactoring, in the invest() function, is the first piece of code about time processing difficult to understand? After refactoring, we abstract this part of the logic into a function and name it isLastDayOfMonth. From the name, we can clearly understand its function and judge whether today is the last day of the month. Here, we have greatly improved the readability of the code by refining the complex logic code into a function.

// 重构前的代码
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. Avoid too many function parameters

I personally think that when the function contains 3 or 4 parameters, it is still acceptable. When it is greater than or equal to 5, we feel that the parameters are a bit too much, which will affect the readability of the code and be inconvenient to use. For the situation of too many parameters, there are generally two processing methods.

  • Consider whether the function has a single responsibility, and whether parameters can be reduced by splitting it into multiple functions. The sample code is as follows:
public void getUser(String username, String telephone, String email);
// 拆分成多个函数
public void getUserByUsername(String username);
public void getUserByTelephone(String telephone);
public void getUserByEmail(String email);
  • Encapsulate function parameters into objects. The sample code is as follows:
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);

In addition, if the function is a remote interface exposed to the outside world, encapsulating the parameters into an object can also improve the compatibility of the interface. When adding new parameters to the interface, the caller of the old remote interface may not need to modify the code to be compatible with the new interface.

3. Do not use function parameters to control logic

Do not use Boolean-type identification parameters in functions to control internal logic. When true, use this logic, and when false, use another logic. This clearly violates the Single Responsibility Principle and the Interface Segregation Principle. I suggest splitting it into two functions for better readability. Let me give you an example to illustrate.

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

However, if the function is a private function with limited scope of influence, or the two functions after splitting are often called at the same time, we can consider retaining the identification parameter as appropriate. The sample code is as follows:
// split into two function calls

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

In addition to the case where the boolean type is used as an identification parameter to control the logic, there is also a case of "according to whether the parameter is null" to control the logic. In this case, we should also split it into multiple functions. The function responsibilities after splitting are clearer and less likely to be used incorrectly. The specific code example is as follows:

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) {
  // ...
}

4. Function design should have a single responsibility

When we talked about the single responsibility principle earlier, we aimed at application objects such as classes and modules. In fact, for the design of functions, the single responsibility principle must be satisfied. Compared with classes and modules, the granularity of functions is relatively small, and the number of lines of code is small. Therefore, when applying the single responsibility principle, it is not as ambiguous as it is applied to classes or modules. It can be as simple as possible.

A specific code example is as follows:

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. Remove too deep nesting levels

Too deep code nesting is often caused by excessive nesting of if-else, switch-case, and for loops. Personally, I suggest that the nesting should not exceed two layers. After more than two layers, you should think about whether you can reduce the nesting. Too deep nesting itself is more difficult to understand. In addition, too deep nesting is easy to indent the code multiple times, causing the statement inside the nest to exceed the length of one line and fold into two lines, which affects the cleanliness of the code. .

The method of solving too deep nesting is relatively mature, and there are the following four common ideas.

  • Remove redundant if or else statements. A code example is as follows:
// 示例一
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;
}
  • Use the continue, break, and return keywords provided by the programming language to exit the nest early. A code example is as follows:
// 重构前的代码
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;
}
  • Adjust execution order to reduce nesting. A specific code example is as follows:
// 重构前的代码
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;
}
  • Encapsulate some nested logic into function calls to reduce nesting. A specific code example is as follows:
// 重构前的代码
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;
}

In addition, there are commonly used methods to replace if-else and switch-case conditional judgments by using polymorphism. This idea involves changes in the code structure, which we will talk about in later chapters, so we won’t explain it here for the time being.

6. Learn to use explanatory variables

Commonly used explanatory variables to improve the readability of the code are the following two.

  • Constants replace magic numbers. The sample code is as follows:
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;
}
  • Use explanatory variables to explain complex expressions. The sample code is as follows:
if (date.after(SUMMER_START) && date.before(SUMMER_END)) {
  // ...
} else {
  // ...
}
// 引入解释性变量后逻辑更加清晰
boolean isSummer = date.after(SUMMER_START)&&date.before(SUMMER_END);
if (isSummer) {
  // ...
} else {
  // ...
} 

key review

Well, that's all for today's content. In addition to the programming skills we talked about today, we also explained naming and annotation, and code style in the first two lessons. Now, let's review the key points of these three lessons together.

1. About naming

  • The key to naming is to be able to express the meaning accurately. For the naming of different scopes, we can choose different lengths appropriately.
  • We can use class information to simplify the naming of attributes and functions, and use function information to simplify the naming of function parameters.
  • Names should be readable and searchable. Don't use uncommon, difficult-to-pronounce English words to name. The naming should conform to the unified specification of the project, and don't use some counter-intuitive naming.
  • There are two naming methods for interfaces: one is to have the prefix "I" in the interface; the other is to have the suffix "Impl" in the implementation class of the interface. For the naming of abstract classes, there are also two ways, one is with the prefix "Abstract", and the other is without the prefix. Both naming methods are acceptable, the key is to be unified in the project.

2. About comments

  • The content of the note mainly includes three aspects: what to do, why, and how to do it. For some complex classes and interfaces, we may also need to write "how to use".

  • Classes and functions must be commented, and written as comprehensively and detailedly as possible. There are relatively few comments inside the function. Generally, good naming, refined functions, explanatory variables, and summary comments are used to improve code readability.

3. About code style

  • How big is the function and class? The number of lines of code of a function should not exceed the size of one screen, such as 50 lines. Class size limits are more difficult to determine.
  • What is the best length for a line of code? It is best not to exceed the display width of the IDE. Of course, it can’t be too small, otherwise many slightly longer statements will be folded into two lines, which will also affect the cleanliness of the code and is not conducive to reading.
  • Make good use of blank lines to separate unit blocks. For relatively long functions, in order to make the logic clearer, blank lines can be used to separate each code block.
  • Four-space indentation or two-space indentation? I personally recommend using two-space indentation, which can save space, especially when the code nesting level is relatively deep. Regardless of whether you use two-space indentation or four-space indentation, you must not use the tab key to indent.
  • Should curly braces start on a new line? Putting the curly braces on the same line as the previous statement saves lines of code. But in the way of opening curly braces on a new line, the left and right brackets can be aligned vertically, and it is more clear at a glance which code belongs to which code block.
  • How are the members of the class arranged? In the Google Java programming specification, dependent classes are arranged alphabetically from small to large. In the class, write member variables first and then write functions. Between member variables or functions, first write static member variables or functions, then write ordinary variables or functions, and arrange them in order according to the size of the scope.

4. About coding skills

  • Distill complex logic into functions and classes.
  • Handle too many parameters by splitting them into multiple functions or encapsulating parameters as objects.
  • Do not use parameters in functions to control code execution logic.
  • Function design should have a single responsibility.
  • Remove too deep nesting levels, methods include: remove redundant if or else statements, use continue, break, return keywords to exit nesting early, adjust execution order to reduce nesting, and abstract part of nesting logic into functions.
  • Replace magic numbers with literal constants.
  • Use explanatory variables to explain complex expressions to improve code readability.

5. Uniform coding standards

In addition to the more detailed knowledge points mentioned in these three sections, in the end, there is another very important thing, that is, projects, teams, and even companies must formulate unified coding standards and supervise their implementation through Code Review. Improving code quality has immediate results.

class disscussion

So far, we have finished talking about the entire 20 coding standards. I wonder how much you have mastered? Apart from the ones I mentioned today, are there any other programming tricks that can significantly improve the readability of code?

Guess you like

Origin blog.csdn.net/qq_32907491/article/details/129891500