A case study of ID generator
Weekly meeting of a department of a company on a certain day:
Boss: “at present our system error is generally by looking at the log to check the problem, but now our system daily log quantity is too large, and the log file of different requests will be interwoven together, there is no way to directly see all the logs of a request, this we have what good method?”
Boss, I think we can borrow the idea of call chain tracing from microservices and assign each request a unique ID that is stored in the context of the request, such as in a ThreadLocal. Each time we print the log, we take the ID from the request context and print it with the log, so that all the logs for the same request contain the same ID and we can search all the logs for the same request by ID.”
Boss: “Well, that works, but how should the ID be generated?”
Wang: “We need to define the format of the ID, for example, divide the ID into three parts, the first part is the last field of the native name, the second part is the current timestamp, and the third part is a random 8-bit string containing upper and lower case letters and numbers, and then use an ID generator to generate the ID.”
Boss: “Won’t the ID generated in this way be repeated?”
Wang: “The ID generated in this way does have the possibility of duplication, but in fact the probability of duplication is very low, for our log tracking requirements, a very small probability of ID duplication is completely acceptable.”
Boss: “ok, then you are responsible for the development of this ID generator.”
Xiao Wang: “Copy that.”
After the weekly meeting, Wang returned to his workstation and began to develop the ID generator. For xiao Wang, who has a little development experience, it is not difficult to implement such an ID generator. Soon, Xiao Wang wrote the code, as follows:
public class IdGenerator {
private static final Logger logger = LoggerFactory.getLogger(IdGenerator.class);
public static String generate(a) {
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);
}
returnid; }}Copy the code
After finishing the code, Wang excitedly sent the code to the boss and asked the boss to do a code review. The boss looked at Wang’s code, frowned, and called back to Wang, telling him that the code was not standard enough and asked him to reconstruct it. Xiao Wang scratched his head, not knowing how to reconstruct it. Suppose we are now Wang’s colleagues, how should we help Wang to refactor such a piece of code?
Before we start refactoring, let’s learn a little bit about refactoring.
Why refactor
First of all, refactoring is an extremely effective way to keep your code quality from becoming hopelessly corrupt. Constantly pile project in evolution, code, if there is no responsibility for the quality of the code, the code is always in the direction of more and more chaotic evolution, when chaos to a certain extent, quantitative change causes the qualitative change, high maintenance costs for the project cost to developing a new code, want to refactor, no one can do it.
Second, refactoring is an effective way to avoid over-design. In the process of maintaining the code, when we really encounter problems, we can effectively avoid investing too much time in the early stage to do excessive design, so as to achieve the target.
Finally, refactoring can help us learn classic design ideas, design principles, design patterns, and programming specifications. Refactoring is actually a good scenario to apply this theoretical knowledge to practice, to practice our ability to skillfully use this theoretical knowledge.
What is refactoring
Software design guru Martin Fowler defines refactoring like this:
Refactoring is an improvement to the internal structure of software to make it easier to understand and less costly to modify without changing its visible behavior.
To put it simply, refactoring means using design ideas, principles, patterns, programming specifications and other theories to optimize code, modify design deficiencies and improve code quality on the premise of keeping functions unchanged.
According to the scale of reconstruction, we can be generally divided into large-scale high-level reconstruction (hereinafter referred to as “large reconstruction”) and small-scale low-level reconstruction (hereinafter referred to as “small reconstruction”) :
- Large refactoring: Large refactoring refers to the refactoring of the top-level code design, including systems, modules, code structures, and the relationships between classes. This kind of refactoring involves many code changes and has a large impact, so it is difficult and time-consuming, and the risk of introducing bugs is relatively high.
- Minor refactoring: Minor refactoring refers to refactoring of code details, mainly at the code level for classes, functions, variables, and so on. This kind of refactoring requires more concentrated modifications, which are relatively simple, highly operable, time-consuming and less likely to introduce bugs.
4. When to refactor
Is the code rotten enough to refactor? Of course not. Because when the code really sucks to the point where “development is slow, lots of people are hired, overtime is done, not much is produced, there are bugs in the line, and engineers complain,” basically refactoring doesn’t solve the problem. Therefore, it is unrealistic to expect a centralized refactoring to solve all problems once the code is rotten enough.
One refactoring strategy, which I personally prefer, is continuous refactoring. In our daily work, we can take a look at what code in the project is not written well enough to be optimized, and take the initiative to refactor it. Or, when modifying or adding a certain functional code, we can also easily refactor the design that does not conform to the code specification and is not good. If we can make continuous refactoring a part of our daily development routine, and cultivate the awareness of continuous refactoring as a development habit, it will benefit both the project and ourselves.
5. How to refactor
According to the scale of refactoring, refactoring can be broadly divided into large refactoring and small refactoring. We need to make a distinction between these two types of refactoring at different scales.
5.1 Large-scale Reconstruction
For large-scale reconstruction, because there will be a lot of modules and codes involved, we need to make a sound reconstruction plan in advance and carry out the reconstruction methodically in stages. In each stage, a small part of the code will be reconstructed, and then it will be submitted, tested and run. After finding no problems, we will continue the reconstruction in the next stage. Ensure that code in the repository is always in a runnable, logically correct state. At each stage, we need to control the scope of the code affected by the refactoring, consider how to accommodate older code logic, and write compatible transition code if necessary.
One of the most effective tools for large refactorings is “decoupling.” So, how do you decouple?
5.1.1 Encapsulation and abstraction
As two very general design ideas, encapsulation and abstraction can be applied in many design scenarios, such as system, module, lib, component, interface, class and so on. Encapsulation and abstraction can effectively hide implementation complexity, isolate implementation variability, and provide a stable and easy-to-use abstract interface to dependent modules.
For example, Unix provides the open() function to open a file, which is very simple to use, but the underlying implementation is very complex, involving permission control, concurrency control, physical storage, and so on. By encapsulating it as an abstract open() function, we can effectively control the spread of code complexity, encapsulating complexity in local code. In addition, because the open() function is defined based on an abstract rather than a concrete implementation, changes to the underlying implementation of the open() function do not require changes to the underlying code that depends on it, ensuring high cohesion and low coupling.
5.1.2 the middle tier
Introducing an intermediate layer simplifies dependencies between modules or classes.
The diagram above shows the dependency comparison before and after the introduction of the middle tier. Before introducing the middle layer of data storage, A, B and C modules all rely on memory storage, Redis storage and DB storage. With the introduction of the middle tier, only one of the three modules needs to rely on the data store. As you can see from the figure, the introduction of the middle tier significantly simplifies dependencies and makes the code structure clearer.
The anticorrosion layer in system architecture design and the facade mode in design mode embody the design idea of middle layer.
5.1.3 modular
Modularization is a common method to construct complex systems. For a large, complex system, no one can control all the details. By dividing the system into separate modules and having different people responsible for different modules, we can coordinate the modules and make the system work effectively without knowing all the details.
Focusing on the code level and reasonably dividing modules can effectively decouple the code and improve the readability and maintainability of the code. Develop code with modularity in mind and treat each module as an independent lib, providing only interfaces to encapsulate the internal implementation details for other modules to use, thus reducing the degree of coupling between different modules.
SOA, microservices, Lib libraries, module partitioning within the system, and even the design of classes and functions all embody the idea of modularity.
5.1.4 Some design ideas and principles
Theory guides practice. In the process of reconstruction, mastering some common design ideas and principles can help us to carry out reconstruction more effectively. Several common design ideas and principles are listed below:
- The single responsibility principle: if a module or class is designed to have a single responsibility, rather than a large one, there will be fewer classes that depend on it and fewer classes that depend on it, and code coupling will be reduced accordingly.
- Interface – rather than implementation-based programming: Isolating change and implementation through an intermediate layer of interfaces has the advantage that changes in one module do not affect the other between the dependent modules.
- Dependency injection: Similar to the idea of interface-based rather than implementation-based programming, dependency injection turns strong coupling between code into weak coupling.
- Use more combination and less inheritance: Inheritance is a strong dependency relationship, and the parent class and the child class are highly coupled, and this coupling relationship is very fragile, affecting the whole body, every change of the parent class will affect all the child class. In contrast, combinatorial relationships are weak dependencies, which are more flexible.
- Demeter’s law: No dependencies between classes that should not have direct dependencies; Try to rely on only the necessary interfaces between classes that have dependencies.
5.2 Minor Reconstruction
If large refactorings require elaborate refactoring plans, complex technical solutions, and a lot of time and effort to complete, small refactorings can improve the quality of code in general through coding specifications.
There are a lot of coding specifications, companies have their own coding specifications, and everyone’s code style is different. Here’s a summary of a few coding specifications that I think work better, and you can also discuss which one is better in the comments section.
5.2.1 named
I put naming at the first point of the coding specification because I think naming is so important. Big to project name, module name, package name, exposed interface name, small to class name, function name, variable name, parameter name, as long as it is doing development, we can not escape the “name”. Naming is very important, even decisive, to the readability of your code.
So, how do you name it?
1, long name or short name?
Long name can contain more information, more intuitively and can accurately express the author’s intentions, however, if the function, the variable name is very long, it made up of their statements will be very long, in the code column length under the condition of limited, will often appear a statement is divided into two rows, it will affect the readability of the code.
Short names, on the other hand, take up less space, but often do not accurately convey the author’s intent, and the various abbreviations involved in naming can be costly to the people reading your code.
I think we can use relatively short names for variables with small scope, such as temporary variables within some functions. On the contrary, I recommend long names for class names that have a larger scope.
2. Use context to simplify naming
Here’s an example:
public class User {
private String userName;
private String userPassword;
private String userAvatarUrl;
/ /...
}
Copy the code
In the above code, there is no need to add user before attributes like userName, because in the context of the user class, name refers to userName, and the implication is clear enough.
Similarly, the naming of function parameters can be simplified using the function name context.
3. Naming interfaces and abstract classes
There are two common naming methods for interfaces. One is to prefix “I” to indicate an Interface, for example, IUserService. The corresponding implementation class is named UserService. The other is unprefixed, such as UserService, with the corresponding implementation class suffixed with “Impl”, such as UserServiceImpl.
Abstract classes can also be named in two ways. One is to prefix Abstract classes with “Abstract”, such as AbstractConfiguration. The other is “Abstract” without the prefix.
In my opinion, it doesn’t matter which way you name it, as long as it’s consistent across teams and projects.
5.2.2 annotation
There is an argument that good naming is a complete substitute for annotation, and if you need an annotation, then the naming is not good enough, and you need to work on naming rather than adding annotations. I think this is a bit extreme for three reasons:
- We can hardly guarantee that our names are canonical names and our code is canonical code.
- Comments carry more information than code: functions and variables that are well named really don’t have to be explained in comments to do what they do. However, for classes, which contain so much information, a simple name is not comprehensive enough.
- Comments serve as summative, documentation: In comments, we can write summative statements or special case statements about specific code implementation ideas. This makes it easier for the reader to read the code by making comments that give the reader an idea of how the code is being implemented.
In addition, some summative comments can make the code structure clearer. For logically complex code or long function, if it is difficult to extract and divide into small function calls, we can use summative comments to make the code structure clearer and more organized, such as:
public boolean isValidPasword(String password) {
// Check whether the password is empty
if (StringUtils.isBlank(password)) {
return false;
}
// Check whether the password length is between 4 and 64 characters
int length = password.length();
if (length < 4 || length > 64) {
return false;
}
// Check whether the password contains only lowercase letters, digits, and decimal points
for (int i = 0; i < length; i++) {
char c = password.charAt(i);
if(! ((c >='a' && c <= 'z') || (c >= '0' && c <= '9') || c == '. ')) {
return false; }}return true;
}
Copy the code
5.2.3 Divide code into smaller unit blocks
Most people have a habit of reading code, looking at the whole first and then the details. Therefore, we need to be modular and abstract thinking, good at refining large chunks of complex logic into classes or functions, shielding details, so that readers do not get lost in the details, which can greatly improve the readability of the code.
Here’s an example:
// The code before refactoring
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;
}
/ /...
}
// Refactored code
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;
}
Copy the code
Before refactoring, was the initial section of the invest() function about time processing hard to understand? After refactoring, we abstracted this logic into a function named isLastDayOfMonth, which gives us a clear idea of what it does and whether today is the last day of the month.
5.2.4 Function design should have a single responsibility
There is no ambiguity when applying the single responsibility principle to functions, as there is when applying it to classes or modules. For example:
public boolean checkUserIfExisting(String telephone, String username, String email) {
if(! StringUtils.isBlank(telephone)) { User user = userRepo.selectUserByTelephone(telephone);returnuser ! =null;
}
if(! StringUtils.isBlank(username)) { User user = userRepo.selectUserByUsername(username);returnuser ! =null;
}
if(! StringUtils.isBlank(email)) { User user = userRepo.selectUserByEmail(email);returnuser ! =null;
}
return false;
}
// Split into three functions
public boolean checkUserIfExistingByTelephone(String telephone);
public boolean checkUserIfExistingByUsername(String username);
public boolean checkUserIfExistingByEmail(String email);
Copy the code
5.2.5 Removing too deep nesting levels
Deep nesting of code is often caused by excessive nesting of if-else, switch-case, and for loops. In addition to the fact that deep nesting itself is difficult to understand, it is easy for code to be indented many times, causing the statements inside the nested to break into two lines over the length of one line, affecting the cleanliness of the code. You can exit nesting early by adjusting the order of execution and using the continue, break, return keywords, for example:
// The code before refactoring
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;
}
// Refactored code
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) {
continue;
}
if(str.contains(substr)) { matchedStrings.add(str); }}return matchedStrings;
}
Copy the code
5.2.6 Use explanatory variables
Use explanatory variables to explain complex expressions:
if (date.after(SUMMER_START) && date.before(SUMMER_END)) {
// ...
} else {
// ...
}
// The logic is clearer after introducing explanatory variables
boolean isSummer = date.after(SUMMER_START)&&date.before(SUMMER_END);
if (isSummer) {
// ...
} else {
// ...
}
Copy the code
Refactor the ID generator code
Now that we’ve covered some refactoring, let’s put the theory into practice and take a look at how we used refactoring to turn the ID generator code mentioned at the beginning of this article from “working” code to “working” code.
First, let’s revisit the ID generator code:
public class IdGenerator {
private static final Logger logger = LoggerFactory.getLogger(IdGenerator.class);
public static String generate(a) {
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);
}
returnid; }}Copy the code
By looking at the code, we can roughly identify the following problems:
- IdGenerator is designed as an implementation class rather than an interface, and callers rely directly on the implementation rather than the interface, violating the design philosophy of interface-based rather than implementation-based programming.
- The code is not readable. In particular, the random string generated part of the code, on the one hand, the code has no comment at all, the generation algorithm is difficult to read, on the other hand, there are many magic numbers in the code, seriously affecting the readability of the code.
- The getting hostName part of the code does not deal with an empty hostName.
- Although the code does exception handling if the native name is not available, the code handles the exception by spitting it out inside the IdGenerator and then printing an error log, rather than continuing to throw it up.
- Each time the ID is generated, the local name needs to be obtained. Obtaining the host name will be time-consuming. This part can be optimized.
- RandomAscii ranges from 0 to 122, but the usable numbers only contain three sub-intervals (0-9, A-z, a-Z). In extreme cases, many invalid numbers outside the three intervals will be generated randomly, requiring many loops to generate a random string, so the random string generation algorithm can also be optimized.
- In the while loop of generate(), the code inside the three if statements is very similar, and the implementation is a little too complicated. You could actually simplify it further by merging the three ifs together.
Did not expect that a code of only 30 lines should be reviewed by us out of 7 problems, Wang’s heart must be broken……
That’s ok. Let’s refactor the code step by step.
6.1 Improve code readability
First, let’s address the most obvious code readability issue that needs to be improved. The specific points are as follows:
- The hostName variable should not be used twice, especially if the two uses have different meanings.
- Extract the code that gets hostName and define it as getLastfieldOfHostName();
- Remove magic numbers from code, for example, 57, 90, 97, 122
- The random number generation code is extracted and defined as generateRandomAlphameric() function;
- The three if logic in generate() is repetitive and the implementation is too complex, so we need to simplify it;
- Rename the IdGenerator class and abstract out the corresponding interface.
Let’s focus on the last change here. In fact, there are three types of naming for ID generator code:
Let’s take a look at each of these names.
The first naming method, which is probably the first that comes to mind, is to name the interface IdGenerator and the implementation class LogTraceIdGenerator. When naming, consider how the next two classes will be used and extended. From the point of view of usage and extension, such naming does not make sense. There are two reasons:
- First of all, if we extend the new log ID generation algorithm, that is, create a new implementation class, because the original implementation class is already called LogTraceIdGenerator, which is too generic, the new implementation class will not be easy to name. Cannot have a parallel name to LogTraceIdGenerator.
- Second, you might say, suppose we don’t have the need to extend log ids, but to extend ID generation algorithms for other businesses, such as UserldGenerator for users, OrderIdGenerator for orders, Is the first nomenclature reasonable? The answer is also no. The main purpose of interface-based programming, rather than implementation programming, is to facilitate flexible replacement of implementation classes later on. The LogTraceIdGenerator, UserIdGenerator, and OrderIdGenerator classes are named for completely different services and do not replace each other. That is, we cannot make this substitution in our logging code. So it doesn’t really make sense for all three classes to implement the same interface.
Is the second naming method reasonable? The answer is also no. LogTraceIdGenerator interface of naming is reasonable, but HostNameMillisIdGenerator implementation class exposes many implementation details, as long as the slight changes in the code, may need to change name, can match.
The third naming method is the one I prefer. In the current ID generator code implementation, the ID we generate is a random ID, not incremental order, so it is reasonable to name it RandomIdGenerator, even if the internal generation algorithm changes, as long as the generated is still a random ID, there is no need to change the name. If we need to extend a new ID generation algorithm, such as an incrementally ordered ID generation algorithm, we can name it SequenceIdGenerator.
In fact, a better way of naming it is to abstract out two interfaces, IdGenerator and LogTraceIdGenerator, and LogTraceIdGenerator inherits IdGenerator. Implementation class interface LogTraceIdGenerator, named RandomIdGenerator, SequenceIdGenerator, etc. In this way, the implementation classes can be reused in multiple business modules, such as users and orders mentioned earlier.
According to the above optimization strategy, we carried out the first round of code reconstruction, and the reconstructed code is as follows:
public class RandomIdGenerator implements LogTraceIdGenerator {
private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);
@Override
public String generate(a) {
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(a) {
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;
}
private String getLastSubstrSplittedByDot(String hostName) {
String[] tokens = hostName.split("\.");
String substrOfHostName = tokens[tokens.length - 1];
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 newString(randomChars); }}Copy the code
6.2 Reconstructing the Exception Handling Code
Next, we’ll look at how the code handles exceptions. For example, what should the ID generator’s generate() function return if the native name fails to get? Is the exception? Null character? Or null? Or some other special value?
Before we talk about specific exception handling, let’s talk about how to handle function errors in general. There are four common types of data returned by functions: error codes, null values, null objects, and exception objects.
Error code
C has no syntax mechanism such as exceptions, so returning error codes is the most common way to deal with errors. In newer languages such as Java and Python, exceptions are used to handle function errors in most cases, and error codes are rarely used. Because the main language we use in our daily work is Java, so we will not do too much introduction of error code, interested partners can consult information.
2, null,
In most programming languages, we use null to denote the semantics of “nonexistence.” However, many people on the Web do not recommend returning null as a bad design idea for two main reasons:
- A null Pointer Exception (NPE) may be thrown if a function returns a null value and fails to check for null values.
- If we define a lot of functions that may return null, our code will be riddled with null-determination logic, which is cumbersome to write and coupled with normal business logic, which will affect the readability of our code.
Here’s an example:
public class UserService {
private UserRepo userRepo; // dependency injection
public User getUser(String telephone) {
// ...
// NullReturn NULL if the user does not exist;}}// Use the function getUser()
User user = userService.getUser("130xxxx0605");
if(user ! =null) { // Check for null values. Otherwise, NPE may be reported
String email = user.getEmail();
if(email ! =null) { // Check for null values. Otherwise, NPE may be reported
String escapedEmail = email.replaceAll("@"."#"); }}Copy the code
Can we use an exception instead of null to throw a UserNotFoundException when the user does not exist?
In my opinion, despite the drawbacks of returning null, the absence of data from lookup functions that begin with the words GET, find, query, etc., is not an exception, it is normal behavior. Therefore, it makes more sense to return null values representing non-existent semantics than to return exceptions.
However, this is not absolute and depends on how other similar lookup functions are defined in the project, as long as the entire project follows a common convention.
3. Empty objects
When the data returned by a function is a string or collection, we can replace the null value with an empty string or collection to indicate that the value does not exist. In this way, we can use the function without null judgment.
Here’s an example:
// Use an empty collection instead of null
public class UserService {
private UserRepo userRepo;
public List<User> getUsers(String telephonePrefix) {
// No data was found
returnCollections.emptyList(); }}// getUsers Example
List<User> users = userService.getUsers("1300605");
for (User user : users) { // There is no need for null judgment
// ...
}
Copy the code
4. Exception object
Although I’ve covered a number of return data types for function errors, the most common way to handle a function error is to throw an exception. Exceptions can carry more error information, such as function call stack information. In addition, exceptions separate the processing of normal logic from exception logic, which makes code more readable.
Exceptions in Java fall into two broad categories:
- Runtime exception (non-checked exception)
- Compile-time exception (checked exception)
I don’t want to go into too much detail about these two exceptions, but there are a lot of information available online. Here we focus on how to handle exceptions thrown by functions.
There are generally three ways to handle exceptions thrown by functions:
- Directly swallow
public void func1(a) throws Exception1 {
// ...
}
public void func2(a) {
/ /...
try {
func1();
} catch(Exception1 e) {
log.warn("...", e);
}
/ /...
}
Copy the code
- re-throw
public void func1(a) throws Exception1 {
// ...
}
public void func2(a) throws Exception1 {
/ /...
func1();
/ /...
}
Copy the code
- Wrap as a new exception re-throw
public void func1(a) throws Exception1 {
// ...
}
public void func2(a) throws Exception2 {
/ /...
try {
func1();
} catch(Exception1 e) {
throw new Exception2("...", e);
}
/ /...
}
Copy the code
Which of the above should we choose when faced with a function that throws an exception? I’ve compiled the following three guidelines based on what I’ve heard online:
- If the exception thrown by func1() is recoverable and the caller of func2() does not care about it, we can simply swallow the exception thrown by func1() inside func2().
- If the exception thrown by func1() is also understandable, of interest to the caller of Func2 (), and of some relevance to the business concept, we can choose to re-throw the exception thrown by Func1 directly.
- If func1() throws an exception that is too low-level for the caller of Func2 () to understand and is conceptually irrelevant to the business, we can repackage it as a new exception that the caller can understand and re-throw it.
All right, so with that said, let’s refactor the exception handling part of the previous code. Take a look at the part of the code that gets the host name:
The generate () :
public String generate(a) {
String substrOfHostName = getLastFieldOfHostName();
long currentTimeMillis = System.currentTimeMillis();
String randomString = generateRandomAlphameric(8);
String id = String.format("%s-%d-%s", substrOfHostName, currentTimeMillis, randomString);
return id;
}
Copy the code
GetLastFieldOfHostName () :
private String getLastFieldOfHostName(a) {
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;
}
Copy the code
In the generate() function, if substrOfHostName returns NULL, assuming the host name fails to get, the generate() function returns data like “null-16723733647-83ab3UK6”. If substrOfHostName returns an empty string, the generate() function returns something like “-16723733647-83ab3UK6.” Neither of these types of data is what we expect to return to the user, so we need to throw an exception when hostname retrieval fails.
If the getLastFieldOfHostName() function fails to get the host name, the UnknownHostException will be caught and an error log will be printed, returning a null value. This is simply the UnknownHostException. Since the host name retrieval failure affects subsequent logic execution, this is an exception and we should throw an exception up instead of returning NULL.
Whether the UnknownHostException is thrown directly or repackaged as a new exception depends on whether the function has a business relevance to the exception. The getLastFieldOfHostName() function is used to get the last field of the host name. UnknownHostException indicates that the host name fails to be obtained by UnknownHostException. There is no need to rewrap it into a new exception.
The refactored getLastFieldOfHostName() function looks like this:
private String getLastFieldOfHostName(a) throws UnknownHostException{
String substrOfHostName = null;
String hostName = InetAddress.getLocalHost().getHostName();
substrOfHostName = getLastSubstrSplittedByDot(hostName);
return substrOfHostName;
}
Copy the code
After the getLastFieldOfHostName() function is modified, the generate() function should also be modified accordingly. Here we select getLastFieldOfHostName () throws UnknownHostException anomalies, and wrapped into a new exception IdGenerationFailureException throw up. There are three reasons for this:
- The caller needs only to know that generate() generates a random unique ID and doesn’t care how it was generated. In other words, it relies on abstraction rather than implementation programming. If the generate() function throws UnknownHostException directly, it exposes implementation details.
- In terms of code encapsulation, we don’t want to expose UnknownHostException, a low-level exception, to higher-level code that calls generate(). Furthermore, the caller does not understand what the exception represents or what to do with it when he or she gets it.
- UnknownHostException is not conceptually related to generate().
The code for the refactored generate() function looks like this:
public String generate(a) throws IdGenerationFailureException {
String substrOfHostName = null;
try {
substrOfHostName = getLastFieldOfHostName();
} 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;
}
Copy the code
For getLastSubstrSplittedByDot () and generateRandomAlphameric () function, we also need to illegally into the reference check to throw an exception when into illegal refs.
At this point, we have refactored the ID generator code as follows. You can compare the refactored code to see if it is more readable, maintainable, and robust than the previous code:
/** * ID generator, used to generate a random ID. * * * The ids generated by this class are not absolutely unique, but the probability of repetition is very small. * /
public class RandomIdGenerator implements IdGenerator {
private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);
/** * generate a random ID. * *@returnA random ID */
@Override
public String generate(a) throws IdGenerationFailureException {
String substrOfHostName = null;
try {
substrOfHostName = getLastFieldOfHostName();
} 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;
}
/** * gets the last field of the localhost name. Host name fields are separated by a '.'. * *@returnThe last field of the host name. Returns null if host name retrieval fails. * /
private String getLastFieldOfHostName(a) throws UnknownHostException{
String substrOfHostName = null;
String hostName = InetAddress.getLocalHost().getHostName();
if (hostName == null || hostName.isEmpty()) {
throw new UnknownHostException("...");
}
substrOfHostName = getLastSubstrSplittedByDot(hostName);
return substrOfHostName;
}
/** * get {@hostname}, separated by a '.'. * *@paramHostName cannot be empty *@return {@hostname} is the last field. When {@hostname} is null and returns an empty string. * /
private String getLastSubstrSplittedByDot(String hostName) {
if (hostName == null || hostName.isEmpty()) {
throw new IllegalArgumentException("...");
}
String[] tokens = hostName.split("\.");
String substrOfHostName = tokens[tokens.length - 1];
return substrOfHostName;
}
/** * generates a random string that contains only numbers, uppercase letters, and lowercase letters. * *@paramLength cannot be less than 0 *@returnRandom string. When {@length} is 0 and returns an empty string. * /
private 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 newString(randomChars); }}Copy the code
Seven,
That concludes this article’s introduction to refactoring. What I want to say is that in our daily work, due to the rapid business iteration and frequent output, many people tend to ignore code specifications and code quality and pile up “bad” code endlessly. When the system bugs occur frequently, the code is difficult to maintain and the development efficiency is reduced, we want to reconstruct, but we have no way to start. In fact, when we write code, we should cultivate a sense of continuous refactoring. No matter how simple the code is, no matter how perfect the code looks, there is always room for optimization as long as we put effort into it. It depends on whether you are willing to do things to the extreme. As a programmer, at least the pursuit of code ah, or with salt fish what difference?
Eight, reference
- Refactoring: Improving the Design of Existing Code
- The Beauty of Design Patterns
- The Code Clean Way
- Code refactoring
- Refactoring introduction and principles