Clean Code Applied to JavaScript - Part VI. Avoid conditionals Complexity
• • 5 min readThis post is the sixth of an interesting series of posts that will delve into the well-known topic that is "Clean Code" but applied to JavaScript.
In this series, we are going to discuss the classic tips around clean code that every programmer should know but applied to a specific JavaScript/TypeScript language.
- Part I. Before your start.
- Part II. Variables.
- Part III. Functions
- Part IV. Comments
- Part V. Exceptions
- Part VI. Avoid conditionals complexity
Introduction
Conditional complexity causes a code to be more complicated to understand and therefore to maintain. In addition, conditional complexity is usually an indicator that the code is coupled. In the case that we want to increase the quality of our code, it is advisable to avoid generating code in which there is conditional complexity.
This post will present some of the techniques and recommendations that can be applied to any code to avoid conditional complexity. In this specific case, we will work using the JavaScript/TypeScript programming language but the concepts we are discussing in this post are extrapolated to any programming language since what we are doing is giving recommendations and techniques that are not tricks for a specific programming language
Don't use flags as function parameters
The first advice I have to give you to is avoiding complexity is to eliminate flags as parameters of a function. Instead, we must create two functions that implement the logic of our problem, instead of using a single function in which we have the logic of the two functionalities since they are different.
The first example shows how the parameter isPremium
is used, which will make the decision to use one function or another. On the other hand, the most correct path would be the one in which we have a declarative way to describe the two functionalities using two different functions for it.
// Dirty
function book(customer, isPremium) {
// ...
if (isPremium) {
premiumLogic();
} else {
regularLogic();
}
}
// Clean (Declarative way)
function bookPremium (customer) {
premiumLogic();
}
function bookRegular (customer) {
retularLogic();
}
Encapsulate Conditionals
Don't make me think! Please encapsulate the conditions in a function that has semantic value.
In the first example you can see how there is a complex condition that makes anyone think, while in the second example it is easily understandable when reading the name of the function.
if (platform.state === 'fetching' && isEmpty(cart)) {
// ...
}
function showLoading(platform, cart) {
return platform.state === 'fetching' && isEmpty(cart);
}
if (showLoading(platform, cart)) {
// ...
}
Replace nested conditional with Guard Clauses
This advice is vital in the lives of programmers. You should not have nested conditionals. One of the main techniques that allow us to avoid nested conditionals is the guard clauses technique. Just image developing without needing else
keyword perfectly.
The following example shows the guard clauses in a demo code, where the reading of the code has improved considerably with a technique that could be automated even by an IDE. Therefore, do not hesitate to use it when it is interesting, you just have to think the logic to the contrary that they taught you in the programming courses.
If you want to delve deeper into the guard clauses you keep, I recommend you read my specific article: Guard Clauses.
function getPayAmount() {
let result;
if (isDead){
result = deadAmount();
}else {
if (isSeparated){
result = separatedAmount();
} else {
if (isRetired){
result = retiredAmount();
}else{
result = normalPayAmount();
}
}
}
return result;
}
function getPayAmount() {
if (isDead) return deadAmount();
if (isSeparated) return separatedAmount();
if (isRetired) return retiredAmount();
return normalPayAmount();
}
Null-Object Pattern
Another common error that can be seen in a code of a junior programmer is the constant checking of whether the object is null and depending on that check a default action is shown or not. This pattern is known as null-object pattern.
The following example shows how you have to check for each of the objects in an array if the animal is null or not to be able to emit the sound.
On the other hand, if we create an object that encapsulates the behavior of the null object, we will not need to perform said verification, as shown in the code in which the pattern is applied.
class Dog {
sound() {
return 'bark';
}
}
['dog', null].map((animal) => {
if(animal !== null) {
sound();
}
});
class Dog {
sound() {
return 'bark';
}
}
class NullAnimal {
sound() {
return null;
}
}
function getAnimal(type) {
return type === 'dog' ? new Dog() : new NullAnimal();
}
['dog', null].map((animal) => getAnimal(animal).sound());
// Returns ["bark", null]
If you want to go deeper into this pattern, I recommend you read my specific article: Null-Object Pattern.
Remove conditionals using polymorphism
The switch
control structure is a tool that most programmers think is cleaner than nesting if (regardless of whether they have a different behavior) we should think about everything else. If we have a switch
in our code we must think that we have just introduced a great complexity to our code that will eventually make us think too much.
The following example shows the misuse of these conditionals to define the logic of a method based on the type of the object. In this case we can make use of a solution based on inheritance that makes use of polymorphism to avoid this complexity since a class will be created for each of these specific types. In this way we will have a more declarative solution since we will have the definition of the method in each of the types of concrete objects.
function Auto() {
}
Auto.prototype.getProperty = function () {
switch (type) {
case BIKE:
return getBaseProperty();
case CAR:
return getBaseProperty() - getLoadFactor();
case BUS:
return (isNailed) ?
0 :
getBaseProperty(voltage);
}
throw new Exception("Should be unreachable");
};
abstract class Auto {
abstract getProperty();
}
class Bike extends Auto {
getProperty() {
return getBaseProperty();
}
}
class Car extends Auto {
getProperty() {
return getBaseProperty() - getLoadFactor();
}
}
class Bus extends Auto {
getProperty() {
return (isNailed) ?
0 :
getBaseProperty(voltage);
}
}
// Somewhere in client code
speed = auto.getProperty();
Remove conditionals using Strategy pattern (composition)/Command pattern
Other patterns that allow us to avoid conditional complexity from our codes is the application of the Strategy and Command design patterns.
If you want to deepen these two patterns I recommend reading the specific articles in which I have deepened in these patterns: Strategy Pattern and Command Pattern.
In the concrete example that illustrates this section you can see the strategy pattern in which the strategy is selected dynamically. Notice how the complexity of the switch
control structure is eliminated using different strategies to solve this problem.
function logMessage(message = "CRITICAL::The system ..."){
const parts = message.split("::");
const level = parts[0];
switch (level) {
case 'NOTICE':
console.log("Notice")
break;
case 'CRITICAL':
console.log("Critical");
break;
case 'CATASTROPHE':
console.log("Castastrophe");
break;
}
}
const strategies = {
criticalStrategy,
noticeStrategy,
catastropheStrategy,
}
function logMessage(message = "CRITICAL::The system ...") {
const [level, messageLog] = message.split("::");
const strategy = `${level.toLowerCase()}Strategy`;
const output = strategies[strategy](messageLog);
}
function criticalStrategy(param) {
console.log("Critical: " + param);
}
function noticeStrategy(param) {
console.log("Notice: " + param);
}
function catastropheStrategy(param) {
console.log("Catastrophe: " + param);
}
logMessage();
logMessage("CATASTROPHE:: A big Catastrophe");
Conclusions
In this post, we have presented some recommendations for avoid conditional complexity.
Conditional complexity makes the code more complicated to read. In addition, it is usually an indication that the code is coupled and therefore is not very flexible.
In this article, different techniques and recommendations have been presented that allow us to avoid conditional complexity in our code by making it climb a quality step.
Finally, the points we have addressed are the following:
- Don't use flags as function parameters
- Encapsulate conditionals
- Replace Nested Conditional with Guard Clauses
- Remove conditionals using polymorphism
- Remove conditionals using Null-Object Pattern
- Remove conditionals using Strategy Pattern(composition)
- Remove conditionals using Command Pattern