[The Beauty of Design Patterns Design Principles and Thoughts: Specification and Refactoring] 35 | Combat 1 (Part 2): Refactor ID generator code from "usable" to "easy to use"

In the previous lesson, we explained how to find code quality issues in conjunction with the ID generator code. Although the requirements of the ID generator are very simple and the number of lines of code is not many, the seemingly simple code actually has a lot of room for optimization. In terms of comprehensive evaluation, Xiao Wang's code can only be regarded as "usable" and barely passed. Most of us can write code to this extent. If we want to stand out in the team, we can't just be satisfied with this 60-point pass. We have to do better for things that everyone can do.

In the last lesson, we talked about why this code can only score 60 points. In this lesson, we will talk about how to refactor the code with 60 points into 80 points and 90 points, so that it can change from "usable" to It has to be "easy to use". Without further ado, let's officially start today's study!

Review code and create a refactoring plan

In order to facilitate your viewing and comparison, I copied the code from the previous lesson here.

public class IdGenerator {
  private static final Logger logger = LoggerFactory.getLogger(IdGenerator.class);
  public static String generate() {
    String id = "";
    try {
      String hostName = InetAddress.getLocalHost().getHostName();
      String[] tokens = hostName.split("\\.");
      if (tokens.length > 0) {
        hostName = tokens[tokens.length - 1];
      }
      char[] randomChars = new char[8];
      int count = 0;
      Random random = new Random();
      while (count < 8) {
        int randomAscii = random.nextInt(122);
        if (randomAscii >= 48 && randomAscii <= 57) {
          randomChars[count] = (char)('0' + (randomAscii - 48));
          count++;
        } else if (randomAscii >= 65 && randomAscii <= 90) {
          randomChars[count] = (char)('A' + (randomAscii - 65));
          count++;
        } else if (randomAscii >= 97 && randomAscii <= 122) {
          randomChars[count] = (char)('a' + (randomAscii - 97));
          count++;
        }
      }
      id = String.format("%s-%d-%s", hostName,
              System.currentTimeMillis(), new String(randomChars));
    } catch (UnknownHostException e) {
      logger.warn("Failed to get the host name.", e);
    }
    return id;
  }
}

When we talked about system design and implementation earlier, we talked about step by step and small steps. The process of refactoring code should also follow this line of thinking. Change a little bit each time, and after the improvement is made, the next round of optimization will be carried out to ensure that each change to the code will not be too large and can be completed in a short period of time. Therefore, we divided the code quality problems found in the previous lesson into four refactorings, as shown below.

  • The first round of refactoring: improving the readability of the code
  • The second round of refactoring: improving the testability of the code
  • The third round of refactoring: writing perfect unit tests
  • The fourth round of refactoring: add comments after all refactorings are completed

The first round of refactoring: improving the readability of the code

First, we'll address the most obvious and most urgent areas of code readability. Specifically, there are the following points:

  • The hostName variable should not be reused, especially when the meaning of the two usages is different;
  • Extract the code for obtaining hostName and define it as the getLastfieldOfHostName() function;
  • Remove the magic numbers in the code, for example, 57, 90, 97, 122;
  • Extract the code of random number generation and define it as generateRandomAlphameric() function;
  • The three if logics in the generate() function are repeated, and the implementation is too complicated, so we need to simplify it;
  • Rename the IdGenerator class and abstract the corresponding interface.

Here we focus on the last modification. In fact, for the code of the ID generator, there are the following three kinds of naming methods. Which do you think is more suitable?
insert image description here

Let's analyze the three naming methods one by one.

In the first naming method, the interface is named IdGenerator, and the implementation class is named LogTraceIdGenerator. This may be the first naming method that many people think of. When naming, we have to consider how the two classes will be used and extended in the future. From the perspective of use and expansion, such a naming is unreasonable.

First of all, if we extend the new log ID generation algorithm, that is to create another new implementation class, because the original implementation class is already called LogTraceIdGenerator, and the name is too general, then the new implementation class is not easy to name, and cannot Take a name parallel to LogTraceIdGenerator.

Secondly, you may say, assuming that we do not have the expansion requirements for log IDs, but we want to expand ID generation algorithms for other businesses, such as for users (UserldGenerator) and orders (OrderIdGenerator), is the first naming method reasonable? Woolen cloth? The answer is no. Based on interface rather than implementation programming, the main purpose is to facilitate subsequent flexible replacement of implementation classes. From the point of view of naming, LogTraceIdGenerator, UserIdGenerator, and OrderIdGenerator involve completely different businesses, and there is no mutual replacement scenario. In other words, it is impossible for us to perform the following substitutions in the log-related code. Therefore, it is actually meaningless for these three classes to implement the same interface.

IdGenearator idGenerator = new LogTraceIdGenerator();
替换为:
IdGenearator idGenerator = new UserIdGenerator();

Is the second naming method reasonable? The answer is no. Among them, the naming of the LogTraceIdGenerator interface is reasonable, but the implementation class of HostNameMillisIdGenerator exposes too many implementation details. As long as the code is slightly changed, the naming may need to be changed to match the implementation.
The third naming method is what I recommend. In the current ID generator code implementation, the ID we generate is a random ID, not an incremental order, so it is more reasonable to name it RandomIdGenerator, even if the internal generation algorithm is changed, as long as the generated ID is still random , there is no need to change the name. If we need to extend a new ID generation algorithm, such as implementing an incrementally ordered ID generation algorithm, we can name it SequenceIdGenerator.
In fact, a better naming method is that we abstract two interfaces, one is IdGenerator and the other is LogTraceIdGenerator, and LogTraceIdGenerator inherits IdGenerator. The implementation class implements the interface IdGenerator, named RandomIdGenerator, SequenceIdGenerator, etc. In this way, the implementation class can be reused in multiple business modules, such as the aforementioned users and orders.
According to the optimization strategy above, we refactor the code for the first round. The code after refactoring is as follows:
public interface IdGenerator { String generate(); } public interface LogTraceIdGenerator extends IdGenerator { }




public class RandomIdGenerator implements IdGenerator {
private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);
@Override
public String generate() {
String substrOfHostName = getLastfieldOfHostName();
long currentTimeMillis = System.currentTimeMillis();
String randomString = generateRandomAlphameric(8);
String id = String.format(“%s-%d-%s”,
substrOfHostName, currentTimeMillis, randomString);
return id;
}
private String getLastfieldOfHostName() {
String substrOfHostName = null;
try {
String hostName = InetAddress.getLocalHost().getHostName();
String[] tokens = hostName.split(“\.”);
substrOfHostName = tokens[tokens.length - 1];
return substrOfHostName;
} catch (UnknownHostException e) {
logger.warn(“Failed to get the host name.”, e);
}
return substrOfHostName;
}
private 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); } } //Code example LogTraceIdGenerator logTraceIdGenerator = new RandomIdGenerator() ; The second round of refactoring: improving the testability of the code Regarding the testability of the code, it mainly includes the following two aspects: The generate() function is defined as a static function, which will affect the testability of the code using this function; The code implementation of the () function depends on the operating environment (local machine name), time function, and random function, so the testability of the generate() function itself is not good. For the first point, we have already solved it in the first round of refactoring. We redefine the generate() static function in the RandomIdGenerator class as a normal function. The caller can create the RandomIdGenerator object externally and inject it into his own code through dependency injection, so as to solve the problem that the static function call affects the testability of the code. For the second point, we need to refactor on the basis of the first round of refactoring. The code after refactoring is shown below, mainly including the following code changes.















From the getLastfieldOfHostName() function, strip out the part of the logic that is more complicated, and define it as the getLastSubstrSplittedByDot() function. Because the getLastfieldOfHostName() function relies on the local host name, this function becomes very simple after stripping out the main code, so you don't need to test it. We focus on testing the getLastSubstrSplittedByDot() function.
Set the access permissions of the functions generateRandomAlphameric() and getLastSubstrSplittedByDot() to protected. The purpose of this is that the two functions can be called directly through the object in the unit test for testing.
Add Google Guava's annotation @VisibleForTesting to generateRandomAlphameric() and getLastSubstrSplittedByDot(). This annotation has no practical effect, it only serves as an identification, telling others that these two functions should have private access rights, and the reason why the access rights are raised to protected is only for testing and can only be used for unit testing middle.
public class RandomIdGenerator implements IdGenerator { private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class); @Override public String generate() {



String substrOfHostName = getLastfieldOfHostName();
long currentTimeMillis = System.currentTimeMillis();
String randomString = generateRandomAlphameric(8);
String id = String.format(“%s-%d-%s”,
substrOfHostName, currentTimeMillis, randomString);
return id;
}
private String getLastfieldOfHostName() {
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) ; It turns out that the Logger object that prints the log is defined as static final and created inside the class. Does this affect the testability of the code? Should the Logger object be injected into the class through dependency injection? The reason why dependency injection can improve code testability is mainly because, in this way, we can easily replace the real objects that depend on with mock objects. So why do we mock this object? This is because this object participates in logic execution (for example, we need to rely on the data it outputs for subsequent calculations) but is uncontrollable. For the Logger object, we only write data into it, do not read the data, do not participate in the execution of business logic, and will not affect the correctness of the code logic, so we do not need to mock the Logger object. In addition, some value objects that are only used to store data, such as String, Map, and UseVo, do not need to be created through dependency injection, and can be created directly in the class through new. The third round of refactoring: writing a perfect unit test After the above refactoring, the obvious problems in the code have basically been solved. We now have unit tests for code completion. There are 4 functions in the RandomIdGenerator class. public String generate();













private String getLastfieldOfHostName();
@VisibleForTesting
protected String getLastSubstrSplittedByDot(String hostName);
@VisibleForTesting
protected String generateRandomAlphameric(int length);
Let's look at the latter two functions first. The logic contained in these two functions is more complicated, which is the focus of our test. Moreover, in the previous step of refactoring, in order to improve the testability of the code, we have isolated these two parts of the code from uncontrollable components (local name, random function, time function). Therefore, we only need to design a complete unit test case. The specific code is implemented as shown below (note that we use the Junit test framework):
Public Class RandomidGERATESTEST { @Test Public Void TestgetSubstrsPlittedBydot () { Randomidgenrator Idgenters = N ew randomidgenrator (); string actualsubstr = IDGENERATOR.GetlastSubstrsplittedBydot ("Field1.Field2. field3”); Assert.assertEquals(“field3”, actualSubstr);





actualSubstr = idGenerator.getLastSubstrSplittedByDot(“field1”);
Assert.assertEquals(“field1”, actualSubstr);
actualSubstr = idGenerator.getLastSubstrSplittedByDot(“field1#field2$field3”);
Assert.assertEquals(“field1#field2#field3”, actualSubstr );
}
// This unit test will fail because we did not handle the case where hostName is null or an empty string in the code
// This part of optimization is left to be explained in the 36th and 37th lessons
@Test
public void testGetLastSubstrSplittedByDot_nullOrEmpty() { RandomIdGenerator idGenerator = new RandomIdGenerator(); String actualSubstr = idGenerator.getLastSubstrSplittedByDot(null); Assert.assertNull(actualSubstr); actualSubstr = idGenerator.getLastSubstrSplittedByDot(""); Assert.assertEquals("", actualSubstr); } @Test







public void testGenerateRandomAlphameric() { RandomIdGenerator idGenerator = new RandomIdGenerator(); String actualRandomString = idGenerator.generateRandomAlphameric(6); Assert.assertNotNull(actualRandomString); Assert.assertEquals(6, actualRandom.length()String); for (char c : actualRandomString .toCharArray()) { Assert.assertTrue(('0' < c && c > '9') || ('a' < c && c > 'z') || ('A' < c && c < ' Z')); } } // This unit test will fail because we did not handle the case of length<=0 in the code // This part of the optimization is left in the 36th and 37th lessons to explain @Test public void testGenerateRandomAlphameric_lengthEqualsOrLessThanZero() { RandomIdGenerator idGenerator = new RandomIdGenerator();













String actualRandomString = idGenerator.generateRandomAlphameric(0);
Assert.assertEquals(“”, actualRandomString);
actualRandomString = idGenerator.generateRandomAlphameric(-1);
Assert.assertNull(actualRandomString);
}
}
Let’s look at the generate() function again. This function is also the only public function we expose for external use. Although the logic is relatively simple, it is best to test it. But, it relies on hostname, random function, time function, how can we test it? Do I need to mock the implementation of these functions?
Actually, it depends on the situation. As we said earlier, when writing unit tests, the test object is the function defined by the function, not the specific implementation logic. In this way, we can achieve that after the implementation logic of the function changes, the unit test cases can still work. What is the function of the generate() function? This is entirely up to the code writer to define.
For example, for the code implementation of the same generate() function, we can have 3 different function definitions, corresponding to 3 different unit tests.
If we define the function of the generate() function as: "generate a random unique ID", then we only need to test whether the ID generated by calling the generate() function multiple times is unique.
If we define the function of the generate() function as: "Generate a unique ID that only contains numbers, uppercase and lowercase letters, and dashes", then we not only need to test the uniqueness of the ID, but also test whether the generated ID contains only Numbers, uppercase and lowercase letters, and dashes.
If we define the function of the generate() function as: "Generate a unique ID, the format is: {host name substr}-{time stamp}-{8-digit random number}. When the host name fails to be obtained, return: null-{ Timestamp}-{8-digit random number}", then we not only need to test the uniqueness of the ID, but also test whether the generated ID fully complies with the format requirements.
To sum up, how to write unit test cases depends on how you define functions. For the first two definitions of the generate() function, we don't need to mock the hostname acquisition function, random function, time function, etc., but for the third definition, we need to mock the hostname acquisition function and let it return null, and the test code runs Is it as expected.
Finally, let's look at the getLastfieldOfHostName() function. In fact, this function is not easy to test, because it calls a static function (InetAddress.getLocalHost().getHostName();), and this static function depends on the runtime environment. However, the implementation of this function is very simple, and the naked eye can basically rule out obvious bugs, so we don't need to write unit test code for it. After all, our purpose of writing unit tests is to reduce code bugs, not to write unit tests for the sake of writing unit tests.
Of course, if you really want to test it, we have a way. One approach is to use a more advanced testing framework. For example, PowerMock, which can mock static functions. Another way is to repackage the logic of obtaining the host name as a new function. However, the latter method will cause the code to be too fragmented, and will also slightly affect the readability of the code. This requires you to weigh the pros and cons to make a choice.
The fourth round of refactoring: adding comments
We mentioned earlier that annotations should not be too many or too few, and are mainly added to classes and functions. Some people say that good naming can replace comments and clearly express meaning. This is true for variable naming, but not necessarily true for classes or functions. The logic contained in a class or function is often complex, and it is difficult to clearly indicate what function is realized by simply naming it. At this time, we need to supplement it through comments. For example, the three function definitions of the generate() function we mentioned earlier cannot be reflected by naming, and need to be added to the comments.
For how to write comments, you can refer to our explanation in Lesson 31. To sum up, the main thing is to write clearly: what to do, why, how to do, how to use, explain some boundary conditions and special circumstances, and explain function input, output, and exceptions.
/**

  • Id Generator that is used to generate random IDs.
  • The IDs generated by this class are not absolutely unique,
  • but the probability of duplication is very low.
    /
    public class RandomIdGenerator implements IdGenerator {
    private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);
    /
    *
    • Generate the random ID. The IDs may be duplicated only in extreme situation.
    • @return an random ID
      /
      @Override
      public String generate() {
      //…
      }
      /
      *
    • Get the local hostname and
    • extract the last field of the name string splitted by delimiter ‘.’.
    • @return the last field of hostname. Returns null if hostname is not obtained.
      /
      private String getLastfieldOfHostName() {
      //…
      }
      /
      *
    • Get the last field of {@hostName} splitted by delemiter ‘.’.
    • @param hostName should not be null
    • @return the last field of {@hostName}. Returns empty string if {@hostName} is empty string.
      /
      @VisibleForTesting
      protected String getLastSubstrSplittedByDot(String hostName) {
      //…
      }
      /
      *
    • Generate random string which
    • only contains digits, uppercase letters and lowercase letters.
    • @param length should not be less than 0
    • @return the random string. Returns empty string if {@length} is 0
      */
      @VisibleForTesting
      protected String generateRandomAlphameric(int length) { //… } } Key Recap Well, this is the end of today's content. Let's summarize and review together the key content you need to master. In this lesson, I will take you to refactor the code written by Xiao Wang into a code with a clearer structure, easier to read, and easier to test, and complete the unit test for it. The knowledge points involved in this are all the content we have talked about in the theoretical chapter. I will not take you to review them one by one if they are more detailed and fragmentary. If you are not very clear, you can go back to the previous chapters to review. In fact, through this class, what I want to convey to you is the following development ideas, which I think are more meaningful than explaining specific knowledge points to you. Even for very simple requirements, the codes written by people of different levels may vary greatly. We must pursue the quality of the code, not just make use of it. Spending a little effort to write a piece of high-quality code is more helpful to improve your coding ability than writing 100 pieces of code that can be used. Knowing what it is, knowing why it is, and understanding the evolution process of excellent code design is more valuable than learning excellent design itself. Knowing why you do it is more important than simply knowing how to do it, so as to prevent you from overusing design patterns, ideas and principles. Design ideas, principles, and patterns do not have too many "high-level" things. They are all simple principles, and there are not many knowledge points. The key is to exercise the ability to analyze specific codes and use knowledge points appropriately. project.










      I often say that the competition among masters is all about the details. The big architecture design, layering, and sub-module ideas are actually similar. No project wins by some unknown design, even if there is, it can be learned quickly. Therefore, the key is to see whether the code details are handled well enough. The accumulation of these differences in details will make a qualitative difference in code quality. Therefore, if you want to improve the code quality, you still need to work hard on the details.
      Class Discussion
      What should the generate() function return when it fails to obtain the host name? Is it a special ID, null, an empty character, or an exception? In Xiao Wang's code implementation, the exception of obtaining the host name was thrown out inside IdGenerator, and an alarm log was printed, but no further throwing was made. Is such an exception handled properly?
      In order to hide the details of the code implementation, we replace the getLastSubstrSplittedByDot(String hostName) function with getLastSubstrByDelimiter(String hostName), is this more reasonable? Why?

Guess you like

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