Refactoring: Guard Clauses
• • 4 min readIntroduction
In computer programming, a guard is a boolean expression that must evaluate to true if the program execution is to continue in the branch in question.
Regardless of which programming language is used, guard code or a guard clause is a check of integrity preconditions used to avoid errors during execution. - Wikipedia
The main problems that appear in a code in which the guard clauses technique is not applied are the following:
- Excessive indentation. Excessive use of the control structure if nested means that there is a high level of indentation that makes code reading difficult.
- Relationship between if-else. When there is a large number of separate code fragments between if-else, which are conceptually related to each other, it is necessary to perform the code reading by jumping between the different parts.
- Mental effort. A consequence of the different jumps in the source code causes an extra effort to be generated in the generation of code.
So, the practical application of a guard clause is the following case:
private doSomething() {
if (everythingIsGood()) {
/*
* Lots and lots of code here!!!
*/
return SOME_VALUE;
} else {
return ANOTHER_VALUE; // a special case
}
}
In this case, and most of the time, you must reverse the logic to avoid using the reserved word else
. The previous code would be rewritten as follows:
private doSomething() {
if (!everythingIsGood()){ // <-- this is your guard clause
return ANOTHER_VALUE;
}
/*
* Lots and lots of code here!!!
*/
return SOME_VALUE;
}
Therefore, the particular cases that cause an exit of the method would be placed at the beginning of the method and act as guards in a way that avoid continuing through the satisfactory flow of the method.
In this way, the method is easy to read since the particular cases are at the beginning of the same and the case of satisfactory flow use is the body of the method.
There are detractors of the guard clauses that indicate that there should only be a single exit point in each method and with this technique we find several exit points. It should not be confused with having return everywhere and without control in our methods that will make us have even greater mental effort. But, all the returns are clearly controlled, since they will be found in the guards or at the end of the method.
Below we will see examples of more complex guard clauses in which the reading and understanding of the code is considerably improved.
Example 1: Guard
Imagine that you have to create a method that calculates the cost of the health insurance in which the userID is received as a parameter.
A search in a database is done using this ID to retrieve a user. In case the user does not exist, an exception called UserNotFoundException
will be throwed. If the user exists in the system, the next step is to verify that the user's health insurance corresponds to one of those that are valid for this algorithm: Allianz or AXA. In case the insurance is not valid, an exception called UserInsuranceNotFoundException
must be returned. Finally, this algorithm is only valid for users who are of Spanish nationality. Therefore, you should check again if the user is Spanish to perform insurance calculation or return an exception called UserIsNotSpanishException
function calculateInsurance(userID: number){
const user = myDB.findOne(userID);
if(user){
if(user.insurance === 'Allianz' or user.insurance === 'AXA'){
if(user.nationality === 'Spain'){
const value = /***
Complex Algorithm
*/
return value;
}else{
throw new UserIsNotSpanishException(user);
}
}else{
throw new UserInsuranceNotFoundException(user);
}
}else{
throw new UserNotFoundException('User NotFound!');
}
}
As you can see the code has many levels of indentation. The same version of the previous algorithm is shown below but the guard clauses technique has been applied. This technique allows the code to be more readable. Note that 3 guard clauses have been applied that allow generating alternative paths (throw exceptions) that do not interfere in the algorithm result.
function calculateInsurance(userID: number){
const user = myDB.findOne(userID);
if(!user){
throw new UserNotFoundException('User NotFound!');
}
if(!(user.insurance === 'Allianz' || user.insurance === 'AXA')){
throw new UserInsuranceNotFoundException(user);
}
if(user.nationality !== 'Spanish'){
throw new UserIsNotSpanishException(user);
}
const value = /***
Complex Algorithm
*/
return value;
}
Some questions that must be resolved:
- Why are not there cases of
if-else if
? - Stop thinking! If your code requires cases like
else if
it is because you are breaking the principle of Single Responsibility and the code makes higher level decisions, which should be refactored using techniques such as division into subfunctions or design patterns such as command or strategy. - Negative conditions are not well understood.
- For this we have another refactoring technique called _ method extraction_ which consists in extracting code into functions for reuse or for reading comprehension. In the following example we modified the previous example creating methods that allow a reading and understanding of the code better.
Example 2: Guard + Extraction function
In the use of clause guard the logic of the conditions is normally inverted and, depending on the complexity of the condition, it is quite complex to understand what is being evaluated in that condition.
That is why it is good practice to extract the logic of the conditions in small functions that allow greater readability of the code and, of course, to find bugs in them, since the responsibility of evaluating the condition is being delegated to a specific function.
For our example of medical insurance we can generate the following methods:
isValidInsurance({ insurance }): boolean{
return insurance === 'Allianz' || insurance === 'AXA';
}
isSpanish({ nationality }): boolean {
return nationality !== 'Spanish';
}
It is not necessary to create a function to check if the user exists, since just checking that the user is different to null or undefined is sufficient. Therefore, the resulting code would be the following:
function calculateInsurance(userID: number){
const user = myDB.findOne(userID);
if(!user){
throw new UserNotFoundException('User NotFound!');
}
if(!isValidInsurante(user)){
throw new UserInsuranceNotFoundException(user);
}
if(!isSpanish(user)){
throw new UserIsNotSpanishException(user);
}
const value = /***
Complex Algorithm
*/
return value;
}
Conclusions
There are many practices in order to improve the quality of the code. The most important thing to learn when applying refactoring techniques is that they should be focused on two points, mainly:
- Uncouple the code, which allows small changes do not cause large chained changes throughout the software project.
- Readability, it is very important that developers understand that most of the time of their work is based on reading code, and probably code written by another developer. It is very beneficial in cost / development that a developer does not spend time understanding elementary logics because it is not easy to read.
Refactoring starts from the most elementary point, a simple if, to an architecture pattern. It is important to take care of all aspects of our software development.
More, More and More...
Refactoring.com
Guard - Wikipedia