[The Beauty of Design Patterns Design Principles and Thoughts: Specification and Refactoring] 36 | Actual Combat 2 (Part 1): What should be returned when a program fails? NULL, exception, error code, empty object?

We can divide the results of function operations into two categories. One category is the expected result, which is what the function outputs under normal circumstances. One category is unexpected results, that is, the results output by the function under abnormal (or called error) conditions. For example, in the previous lesson, the function to obtain the local host name, under normal circumstances, the function returns the local host name in string format; under abnormal conditions, if the acquisition of the local host name fails, the function returns the UnknownHostException exception object.

Under normal circumstances, the type of data returned by the function is very clear, but in exceptional cases, the type of data returned by the function is very flexible, and there are many choices. In addition to the exception objects like UnknownHostException just mentioned, functions can also return error codes, NULL values, special values ​​(such as -1), empty objects (such as empty strings, empty collections), etc. in abnormal situations.

Each exception return data type has its own characteristics and applicable scenarios. But sometimes, under abnormal circumstances, it is not so easy to judge what data type the function should return. For example, in the last lesson, what should the generate() function of the ID generator return when the host name failed to be obtained? Is it abnormal? Null character? Or a NULL value? Or other special values ​​(such as null-15293834874-fd3A9KBn, null means that the host name has not been obtained)?

Functions are a very important unit of writing code, and the exception handling of functions is something we must always consider when writing functions. So, today we will talk about how to design the return data type of a function under abnormal circumstances.

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

Let's start with the ID generator code from the previous lesson

In the last two lessons, we refactored a very simple ID generator code from "usable" to "easy to use". The final code seems to be perfect, but if we think about it again, there is still room for further optimization in the way of error handling in the code, which is worth discussing again.
For your convenience, I copied the code from the previous lesson here.

public class RandomIdGenerator implements IdGenerator {
  private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);
  @Override
  public String generate() {
    String substrOfHostName = getLastFiledOfHostName();
    long currentTimeMillis = System.currentTimeMillis();
    String randomString = generateRandomAlphameric(8);
    String id = String.format("%s-%d-%s",
            substrOfHostName, currentTimeMillis, randomString);
    return id;
  }
  private String getLastFiledOfHostName() {
    String substrOfHostName = null;
    try {
      String hostName = InetAddress.getLocalHost().getHostName();
      substrOfHostName = getLastSubstrSplittedByDot(hostName);
    } catch (UnknownHostException e) {
      logger.warn("Failed to get the host name.", e);
    }
    return substrOfHostName;
  }
  @VisibleForTesting
  protected String getLastSubstrSplittedByDot(String hostName) {
    String[] tokens = hostName.split("\\.");
    String substrOfHostName = tokens[tokens.length - 1];
    return substrOfHostName;
  }
  @VisibleForTesting
  protected String generateRandomAlphameric(int length) {
    char[] randomChars = new char[length];
    int count = 0;
    Random random = new Random();
    while (count < length) {
      int maxAscii = 'z';
      int randomAscii = random.nextInt(maxAscii);
      boolean isDigit= randomAscii >= '0' && randomAscii <= '9';
      boolean isUppercase= randomAscii >= 'A' && randomAscii <= 'Z';
      boolean isLowercase= randomAscii >= 'a' && randomAscii <= 'z';
      if (isDigit|| isUppercase || isLowercase) {
        randomChars[count] = (char) (randomAscii);
        ++count;
      }
    }
    return new String(randomChars);
  }
}

There are four functions in this code. For the error handling methods of these four functions, I have summarized the following questions.

  • For the generate() function, what does the function return if the host name fails to be obtained? Is such a return value reasonable?
  • For the getLastFiledOfHostName() function, should the UnknownHostException be swallowed inside the function (try-catch and print the log)? Or should the exception continue to be thrown up? If it is thrown upwards, should the UnknownHostException be thrown as it is, or encapsulated into a new exception?
  • For the getLastSubstrSplittedByDot(String hostName) function, if hostName is NULL or an empty string, what should this function return?
  • For the generateRandomAlphameric(int length) function, if length is less than 0 or equal to 0, what should this function return?

For the above questions, you can try to think about them, I will not answer them first. After we have finished learning the theoretical content of this lesson, we will analyze it together in the next lesson. In this section we focus on some theoretical knowledge.

What should the function return when an error occurs?

Regarding the return data type of the function error, I have summarized 4 situations, which are: error code, NULL value, empty object, and exception object. Next, let's take a look at their usage and applicable scenarios one by one.

1. Return an error code

There is no grammatical mechanism such as exception in C language, so returning an error code is the most commonly used error handling method. In relatively new programming languages ​​such as Java and Python, in most cases, we use exceptions to handle function errors, and error codes are rarely used.

In the C language, there are two ways to return the error code: one is to directly occupy the return value of the function, and the return value of the normal execution of the function is placed in the output parameter; the other is to define the error code as a global variable, in the function When an error occurs, the caller of the function obtains the error code through this global variable. For these two methods, I will give an example to further explain. The specific code is as follows:

// 错误码的返回方式一:pathname/flags/mode为入参;fd为出参,存储打开的文件句柄。
int open(const char *pathname, int flags, mode_t mode, int* fd) {
  if (/*文件不存在*/) {
    return EEXIST;
  }
  
  if (/*没有访问权限*/) {
    return EACCESS;
  }
  
  if (/*打开文件成功*/) {
    return SUCCESS; // C语言中的宏定义:#define SUCCESS 0
  }
  // ...
}
//使用举例
int fd;
int result = open(“c:\test.txt”, O_RDWR, S_IRWXU|S_IRWXG|S_IRWXO, &fd);
if (result == SUCCESS) {
  // 取出fd使用
} else if (result == EEXIST) {
  //...
} else if (result == EACESS) {
  //...
}
// 错误码的返回方式二:函数返回打开的文件句柄,错误码放到errno中。
int errno; // 线程安全的全局变量
int open(const char *pathname, int flags, mode_t mode){
  if (/*文件不存在*/) {
    errno = EEXIST;
    return -1;
  }
  
  if (/*没有访问权限*/) {
    errno = EACCESS;
    return -1;
  }
  
  // ...
}
// 使用举例
int hFile = open(“c:\test.txt”, O_RDWR, S_IRWXU|S_IRWXG|S_IRWXO);
if (-1 == hFile) {
  printf("Failed to open file, error no: %d.\n", errno);
  if (errno == EEXIST ) {
    // ...        
  } else if(errno == EACCESS) {
    // ...    
  }
  // ...
}

In fact, if there is a grammatical mechanism of exceptions in the programming language you are familiar with, try not to use error codes. Compared with error codes, exceptions have many advantages, for example, they can carry more error information (the exception can contain message, stack trace, etc.). Regarding exceptions, we will explain them in great detail later.

2. Return NULL value

In most programming languages, we use NULL to represent the semantics of "not existing". However, many people on the Internet do not recommend that functions return NULL values, thinking that this is a bad design idea. The main reasons are as follows.

  • If a function may return a NULL value, we forget to make a NULL value judgment when we use it, and a Null Pointer Exception (NPE for short) may be thrown.
  • If we define many functions whose return value may be NULL, the code will be filled with a lot of NULL value judgment logic. On the one hand, it is cumbersome to write, and on the other hand, they are coupled with normal business logic, which will affect the code. readability.

Let me explain with an example, the specific code is as follows:

public class UserService {
  private UserRepo userRepo; // 依赖注入
  
  public User getUser(String telephone) {
    // 如果用户不存在,则返回null
    return null;
  }
}
// 使用函数getUser()
User user = userService.getUser("18917718965");
if (user != null) { // 做NULL值判断,否则有可能会报NPE
  String email = user.getEmail();
  if (email != null) { // 做NULL值判断,否则有可能会报NPE
    String escapedEmail = email.replaceAll("@", "#");
  }
}

So can we use exceptions instead of NULL values, and let the function throw UserNotFoundException when the search user does not exist?

Personally, I feel that for find functions starting with words like get, find, select, search, query, etc., the absence of data is not an anomaly, but normal behavior, despite all the drawbacks of returning a NULL value. Therefore, it is more reasonable to return a NULL value representing the absence of semantics than to return an exception.

However, having said that, the reason just mentioned is not particularly convincing. For the case where the search data does not exist, whether the function should use a NULL value or an exception, an important reference standard is to see how other similar search functions in the project are defined, as long as the entire project follows a unified agreement. . If the project is developed from scratch and there is no unified convention and code that can be referred to, then you can choose either of the two. You only need to comment clearly where the function is defined, so that the caller can clearly know what will be returned when the data does not exist.

Let me add that, for the search function, in addition to returning the data object, some will also return the subscript position, such as the indexOf() function in Java, which is used to find another substring in a string The position of the first occurrence. The return value type of the function is the basic type int. At this time, we cannot use the NULL value to represent the non-existence situation. For this situation, we have two ways to deal with it, one is to return NotFoundException, and the other is to return a special value, such as -1. Obviously -1 is more reasonable, though, for the same reason, which means that "not found" is normal rather than abnormal behavior.

3. Return empty object

As we just mentioned, returning NULL values ​​has various disadvantages. A classic strategy to deal with this problem is to apply the Null Object Design Pattern. Regarding this design pattern, we will talk about it in detail in later chapters, so I won’t expand it now. However, today we will talk about two relatively simple and special empty objects, that is, empty strings and empty collections.

When the data returned by the function is a string type or a collection type, we can replace the NULL value with an empty string or an empty collection to indicate that it does not exist. In this way, when we use the function, we don't need to judge the NULL value. Let me give you an example to explain. The specific code is as follows:

// 使用空集合替代NULL
public class UserService {
  private UserRepo userRepo; // 依赖注入
  
  public List<User> getUsers(String telephonePrefix) {
   // 没有查找到数据
    return Collectiosn.emptyList();
  }
}
// getUsers使用示例
List<User> users = userService.getUsers("189");
for (User user : users) { //这里不需要做NULL值判断
  // ...
}
// 使用空字符串替代NULL
public String retrieveUppercaseLetters(String text) {
  // 如果text中没有大写字母,返回空字符串,而非NULL值
  return "";
}
// retrieveUppercaseLetters()使用举例
String uppercaseLetters = retrieveUppercaseLetters("wangzheng");
int length = uppercaseLetters.length();// 不需要做NULL值判断 
System.out.println("Contains " + length + " upper case letters.");

4. throw exception object

Although the return data types of many function errors have been mentioned above, the most commonly used method of function error handling is to throw exceptions. Exceptions can carry more error information, such as function call stack information. In addition, exceptions can separate the processing of normal logic and exception logic, so that the readability of the code will be better.

Different programming languages ​​have slightly different syntax for exceptions. C++ and most dynamic languages ​​(Python, Ruby, JavaScript, etc.) define only one exception type: runtime exception (Runtime Exception). And like Java, in addition to runtime exceptions, another exception type is defined: compile-time exceptions (Compile Exception).

For runtime exceptions, when we write code, we don't need to take the initiative to try-catch. When the compiler compiles the code, it will not check whether the code has handled runtime exceptions. On the contrary, for compile-time exceptions, when we write code, we need to take the initiative to try-catch or declare in the function definition, otherwise the compilation will report an error. Therefore, runtime exceptions are also called unchecked exceptions (Unchecked Exception), and compile-time exceptions are also called checked exceptions (Checked Exception).

If only one exception type is defined in the programming language you are familiar with, it is relatively simple to use. If two exception types are defined in a programming language you are familiar with (such as Java), which exception type should we choose to throw when an exception occurs? Is it a checked exception or an unchecked exception?

For code bugs (such as array out of bounds) and unrecoverable exceptions (such as database connection failure), even if we catch them, we can't do much, so we tend to use unchecked exceptions. For recoverable exceptions and business exceptions, such as exceptions where the withdrawal amount is greater than the balance, we prefer to use checked exceptions to clearly inform the caller that they need to be caught and processed.

Let me explain with an example, the code is as follows. When the address of Redis (parameter address) is not set, we directly use the default address (such as local address and default port); when the address format of Redis is incorrect, we hope that the program can fail-fast, that is, Treat this situation as an unrecoverable exception, throw a runtime exception directly, and terminate the program.

// address格式:"192.131.2.33:7896"
public void parseRedisAddress(String address) {
  this.host = RedisConfig.DEFAULT_HOST;
  this.port = RedisConfig.DEFAULT_PORT;
  
  if (StringUtils.isBlank(address)) {
    return;
  }
  String[] ipAndPort = address.split(":");
  if (ipAndPort.length != 2) {
    throw new RuntimeException("...");
  }
  
  this.host = ipAndPort[0];
  // parseInt()解析失败会抛出NumberFormatException运行时异常
  this.port = Integer.parseInt(ipAndPort[1]);
}

In fact, the checked exceptions supported by Java have been criticized, and many people advocate that all abnormal situations should use unchecked exceptions. There are three main reasons to support this view.

  • Checked exceptions need to be explicitly declared in the function definition. If the function throws many checked exceptions, the definition of the function will be very verbose, which will affect the readability of the code and be inconvenient to use.
  • The compiler forces us to explicitly catch all checked exceptions, and the code implementation will be cumbersome. Unchecked exceptions are just the opposite. We don't need to display the declaration in the definition, and we can freely decide whether we need to catch the exception.
  • The use of checked exceptions violates the open-closed principle. If we add a checked exception to a function, all functions above it in the function call chain where this function is located need to make corresponding code modifications until a function in the call chain adds the newly added exception. Until the exception try-catch is disposed of. Adding an unchecked exception does not need to change the code on the calling chain. We can flexibly choose to centralize processing in a certain function, such as centralizing exception processing in the AOP aspect in Spring.

However, unchecked exceptions also have disadvantages, and its advantages are actually its disadvantages. From the statement just now, we can see that unchecked exceptions are more flexible to use, and the initiative of how to deal with them is left to the programmer. We also mentioned earlier that being too flexible will lead to uncontrollability. Unchecked exceptions do not need to be explicitly declared in the function definition. When we use a function, we need to check the code to know which exceptions will be thrown. Unchecked exceptions do not need to be caught and processed, and programmers may miss some exceptions that should be caught and processed.

There is a lot of debate online about whether to use checked or unchecked exceptions, but there isn't a very strong reason why one is necessarily better than the other. Therefore, we only need to formulate a unified exception handling specification in the same project according to the development habits of the team.

We just talked about two types of exceptions, now let's talk about how to deal with exceptions thrown by functions? To sum up, there are generally three processing methods as follows.

  • Swallow it straight up. A specific code example is as follows:
public void func1() throws Exception1 {
  // ...
}
public void func2() {
  //...
  try {
    func1();
  } catch(Exception1 e) {
    log.warn("...", e); //吐掉:try-catch打印日志
  }
  //...
}
  • Re-throw intact. A specific code example is as follows:
public void func1() throws Exception1 {
  // ...
}
public void func2() throws Exception1 {//原封不动的re-throw Exception1
  //...
  func1();
  //...
}
  • Wrapped into a new exception re-throw. A specific code example is as follows:
public void func1() throws Exception1 {
  // ...
}
public void func2() throws Exception2 {
  //...
  try {
    func1();
  } catch(Exception1 e) {
   throw new Exception2("...", e); // wrap成新的Exception2然后re-throw
  }
  //...
}

When we face a function throwing an exception, which of the above processing methods should we choose? I have summarized the following three reference principles:

  • If the exception thrown by func1() can be recovered, and the caller of func2() does not care about this exception, we can completely swallow the exception thrown by func1() in func2();
  • If the exception thrown by func1() is understandable and caring to the caller of func2(), and has certain relevance in business concepts, we can choose to directly re-throw the exception thrown by func1 ;
  • If the exception thrown by func1() is too low-level for the caller of func2() to understand, and the business concept is irrelevant, we can repackage it into a new exception that the caller can understand, and then re- throw.

In short, whether to continue throwing up depends on whether the upper layer code cares about this exception. Throw it out if you care, otherwise just swallow it. Whether it needs to be wrapped into a new exception and thrown depends on whether the upper-level code can understand the exception and whether it is business-related. If it is understandable and business-related, it can be thrown directly, otherwise it will be encapsulated into a new exception and thrown. Regarding this part of theoretical knowledge, we will further explain it in combination with the code of the ID generator in the next lesson.

key review

Well, that's all for today's content. Let's summarize and review together the key content you need to master.

For the data type returned by the function error, I have summarized 4 situations, which are: error code, NULL value, empty object, and exception object.

1. Return an error code

The C language does not have a grammatical mechanism such as exceptions, and returning error codes is the most commonly used error handling method. In relatively new programming languages ​​such as Java and Python, in most cases, we use exceptions to handle function errors, and error codes are rarely used.

2. Return NULL value

In most programming languages, we use NULL to represent the semantics of "not existing". For lookup functions, the absence of data is not an abnormal condition, but a normal behavior, so it is more reasonable to return a NULL value indicating the semantics of non-existence than to return an exception.

3. Return empty object

Returning NULL values ​​has various drawbacks, and there is a more classic countermeasure to this, which is to apply the null object design pattern. When the data returned by the function is a string type or a collection type, we can replace the NULL value with an empty string or an empty collection to indicate that it does not exist. In this way, when we use the function, we don't need to judge the NULL value.

4. throw exception object

Although the return data types of many function errors have been mentioned above, the most common way to handle function errors is to throw exceptions. There are two types of exceptions: checked exceptions and unchecked exceptions.

There is a lot of debate online about whether to use checked or unchecked exceptions, but there isn't a very strong reason why one is necessarily better than the other. Therefore, we only need to formulate a unified exception handling specification in the same project according to the development habits of the team.

For the exception thrown by the function, we have three processing methods: swallow it directly, throw it directly up, and wrap it into a new exception throw. We will leave this part for further explanation in the next lesson combined with actual combat.

class disscussion

Combined with the theoretical knowledge we learned today, try to answer the four questions mentioned at the beginning of the article for RandomIdGenerator.
Welcome to write down your answers in the message area, communicate and share with your classmates. If you gain something, you are welcome to share this article with your friends.

Guess you like

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