[The Beauty of Design Patterns Design Principles and Thoughts: Specification and Refactoring] 37 | Practice 2 (Part 2): Refactoring the exception handling code of each function in the ID generator project

In the previous lesson, we explained how to deal with several abnormal situations, such as returning error codes, NULL values, empty objects, and exception objects. For the most commonly used exception objects, we also focus on the application scenarios of two types of exceptions, and three processing methods for exceptions thrown by functions: direct swallowing, throwing intact and wrapping them into new exceptions out.

In addition, at the beginning of the previous lesson, we also asked 4 questions about exception handling for the code of the ID generator. Today, we will use the time of one class, combined with the theoretical knowledge mentioned in the last class, to answer these questions one by one.

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

Refactor generate() function

First, let's see, for the generate() function, if the local machine name fails to be obtained, what does the function return? Is such a return value reasonable?

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

The ID consists of three parts: local name, timestamp and random number. The generation functions of timestamp and random number will not make mistakes, but the host name may fail to be obtained. In the current code implementation, if the hostname fails to be obtained and substrOfHostName is NULL, the generate() function will return data like "null-16723733647-83Ab3uK6". If the host name fails to be obtained and substrOfHostName is an empty string, the generate() function will return data like "-16723733647-83Ab3uK6".

Is it reasonable to return the above two special ID data formats under abnormal circumstances? This is actually difficult to say, we have to look at how the specific business is designed. However, I'd prefer to explicitly notify the caller of the exception. Therefore, it is better to throw a checked exception instead of a special value here.

According to this design idea, we refactor the generate() function. The code after refactoring looks like this:

  public String generate() throws IdGenerationFailureException {
    String substrOfHostName = getLastFiledOfHostName();
    if (substrOfHostName == null || substrOfHostName.isEmpty()) {
      throw new IdGenerationFailureException("host name is empty.");
    }
    long currentTimeMillis = System.currentTimeMillis();
    String randomString = generateRandomAlphameric(8);
    String id = String.format("%s-%d-%s",
            substrOfHostName, currentTimeMillis, randomString);
    return id;
  }

Refactor getLastFiledOfHostName() function

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?

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

The current processing method is that when the hostname acquisition fails, the getLastFiledOfHostName() function returns a NULL value. As we mentioned earlier, whether to return a NULL value or an abnormal object depends on whether the failure to obtain data is a normal behavior or an abnormal behavior. Failure to obtain the host name will affect the processing of subsequent logic, which is not what we expect, so it is an abnormal behavior. It is better to throw an exception here than to return a NULL value.

As for whether to throw UnknownHostException directly or repackage it into a new exception, it depends on whether the function has a business relationship with the exception. The getLastFiledOfHostName() function is used to obtain the last field of the host name. The UnknownHostException exception indicates that the acquisition of the host name failed. The two are related to business, so UnknownHostException can be thrown directly without repackaging into a new exception.

According to the above design ideas, we refactor the getLastFiledOfHostName() function. The refactored code looks like this:

 private String getLastFiledOfHostName() throws UnknownHostException{
    String substrOfHostName = null;
    String hostName = InetAddress.getLocalHost().getHostName();
    substrOfHostName = getLastSubstrSplittedByDot(hostName);
    return substrOfHostName;
 }

After the getLastFiledOfHostName() function is modified, the generate() function should also be modified accordingly. We need to catch the UnknownHostException thrown by getLastFiledOfHostName() in the generate() function. When we catch this exception, what should we do with it?

According to the previous analysis, when ID generation fails, we need to explicitly inform the caller. Therefore, we cannot swallow the UnknownHostException exception in the generate() function. Then should we throw it intact, or encapsulate it into a new exception throw?

We choose the latter. In the generate() function, we need to catch UnknownHostException and rewrap it into a new exception IdGenerationFailureException and throw it up. This is done for three reasons.

  • When the caller uses the generate() function, he only needs to know that it generates a random unique ID, and does not care how the ID is generated. In other words, this is programming that relies on abstraction rather than implementation. If the generate() function throws UnknownHostException directly, it actually exposes the implementation details.
  • From the perspective of code encapsulation, we don't want to expose UnknownHostException, a relatively low-level exception, to the upper-level code, that is, the code that calls the generate() function. Moreover, when the caller gets this exception, he can't understand what the exception represents, and he doesn't know how to deal with it.
  • The UnknownHostException exception has no correlation with the generate() function in terms of business concepts.

According to the above design ideas, we refactor the generate() function again. The refactored code looks like this:

  public String generate() throws IdGenerationFailureException {
    String substrOfHostName = null;
    try {
      substrOfHostName = getLastFiledOfHostName();
    } catch (UnknownHostException e) {
      throw new IdGenerationFailureException("host name is empty.");
    }
    long currentTimeMillis = System.currentTimeMillis();
    String randomString = generateRandomAlphameric(8);
    String id = String.format("%s-%d-%s",
            substrOfHostName, currentTimeMillis, randomString);
    return id;
  }

Refactor getLastSubstrSplittedByDot() function

For the getLastSubstrSplittedByDot(String hostName) function, if hostName is NULL or an empty string, what should this function return?

  @VisibleForTesting
  protected String getLastSubstrSplittedByDot(String hostName) {
    String[] tokens = hostName.split("\\.");
    String substrOfHostName = tokens[tokens.length - 1];
    return substrOfHostName;
  }

Theoretically speaking, the correctness of parameter passing should be guaranteed by the programmer, and we don't need to make judgments and special handling of NULL values ​​or empty strings. The caller should not have passed a NULL value or an empty string to the getLastSubstrSplittedByDot() function. If passed, it's a code bug and needs to be fixed. But, having said that, there is no guarantee that programmers will not pass NULL values ​​or empty strings. So should we judge whether it is a NULL value or an empty string?

If the function is private to the class and is only called inside the class, it is completely under your own control, and you can ensure that you do not pass NULL values ​​or empty strings when calling this private function. Therefore, we don't need to judge NULL value or empty string in the private function. If the function is public, you have no control over who will call it and how it will be called (it is possible that a colleague may be negligent and pass in a NULL value, and this situation also exists). In order to improve the robustness of the code as much as possible, we best It is best to make a judgment of NULL value or empty string in the public function.

Then you may say that getLastSubstrSplittedByDot() is protected, neither a private function nor a public function, so should we judge whether it is a NULL value or an empty string?

The reason why it is set to protected is to facilitate writing unit tests. However, unit tests may need to test some corner cases, such as the case where the input is a NULL value or an empty string. Therefore, here we'd better also add the judgment logic of NULL value or empty string. Although there is some redundancy, it will not be wrong to add more tests.

According to this design idea, we refactor the getLastSubstrSplittedByDot() function. The code after refactoring looks like this:

  @VisibleForTesting
  protected String getLastSubstrSplittedByDot(String hostName) {
    if (hostName == null || hostName.isEmpty()) {
      throw IllegalArgumentException("..."); //运行时异常
    }
    String[] tokens = hostName.split("\\.");
    String substrOfHostName = tokens[tokens.length - 1];
    return substrOfHostName;
  }
按照上面讲的,我们在使用这个函数的时候,自己也要保证不传递 NULL 值或者空字符串进去。所以,getLastFiledOfHostName() 函数的代码也要作相应的修改。修改之后的代码如下所示:
 private String getLastFiledOfHostName() throws UnknownHostException{
    String substrOfHostName = null;
    String hostName = InetAddress.getLocalHost().getHostName();
    if (hostName == null || hostName.isEmpty()) { // 此处做判断
      throw new UnknownHostException("...");
    }
    substrOfHostName = getLastSubstrSplittedByDot(hostName);
    return substrOfHostName;
 }

Refactor generateRandomAlphameric() function

For the generateRandomAlphameric(int length) function, if length < 0 or length = 0, what should this function return?

  @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);
  }
}

Let's first look at the case of length < 0. Generating a random string with a negative length is illogical and anomalous. So, when the incoming parameter length < 0, we throw an IllegalArgumentException.

Let's look at the case of length = 0 again. Is length = 0 abnormal behavior? It depends on how you define yourself. We can define it as an abnormal behavior, throwing IllegalArgumentException exception, or define it as a normal behavior, let the function return an empty string directly when the input parameter length = 0. No matter which processing method you choose, the most important point is to clearly tell what kind of data will be returned when length = 0 in the function comment.

RandomIdGenerator code after refactoring

This is the end of the refactoring of the exception handling code of each function in the RandomIdGenerator class. For easy viewing, I post the refactored code here after rearranging it. You can compare and see if it is consistent with your refactoring ideas.

public class RandomIdGenerator implements IdGenerator {
  private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);
  @Override
  public String generate() throws IdGenerationFailureException {
    String substrOfHostName = null;
    try {
      substrOfHostName = getLastFiledOfHostName();
    } catch (UnknownHostException e) {
      throw new IdGenerationFailureException("...", e);
    }
    long currentTimeMillis = System.currentTimeMillis();
    String randomString = generateRandomAlphameric(8);
    String id = String.format("%s-%d-%s",
            substrOfHostName, currentTimeMillis, randomString);
    return id;
  }
  private String getLastFiledOfHostName() throws UnknownHostException{
    String substrOfHostName = null;
    String hostName = InetAddress.getLocalHost().getHostName();
    if (hostName == null || hostName.isEmpty()) {
      throw new UnknownHostException("...");
    }
    substrOfHostName = getLastSubstrSplittedByDot(hostName);
    return substrOfHostName;
  }
  @VisibleForTesting
  protected String getLastSubstrSplittedByDot(String hostName) {
    if (hostName == null || hostName.isEmpty()) {
      throw new IllegalArgumentException("...");
    }
    String[] tokens = hostName.split("\\.");
    String substrOfHostName = tokens[tokens.length - 1];
    return substrOfHostName;
  }
  @VisibleForTesting
  protected String generateRandomAlphameric(int length) {
    if (length <= 0) {
      throw new IllegalArgumentException("...");
    }
    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);
  }
}

key review

Well, that's all for today's content. Let's summarize and review together, what you need to focus on.

Today's content is more practical, and it is an application of the theoretical knowledge learned in the previous class. From today's actual combat, what higher-level software design and development ideas have you learned? I am here to throw bricks and start jade, summed up the following 3 points.

  • No matter how simple the code is, no matter how perfect the code looks, as long as we work hard to deliberate, there is always room for optimization, it depends on whether you are willing to do things to the extreme.
  • If your internal skills are not deep enough and your theoretical knowledge is not solid enough, then it will be difficult for you to understand where the code of open source projects is excellent. Just like if we don’t have the previous theoretical study, today I will refactor, explain, and analyze for you bit by bit, just give you the code of the finally refactored RandomIdGenerator, can you really learn the essence of its design?
  • Comparing Xiao Wang's IdGenerator code at the beginning of Lesson 34 and the final RandomIdGenerator code, one of them is "usable" and the other is "easy to use", which is very different. As a programmer, at least you must pursue the code, otherwise what is the difference with salted fish!

class disscussion

We spent 4 lectures doing multiple iterative refactorings of a very simple ID generator code in less than 40 lines. In addition to the points I just mentioned in the "Key Review", what more valuable things have you learned from this iterative refactoring process?

Guess you like

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