loading...
Cover image for Clean Code Applied to JavaScript — Part VI. Avoid Conditional Complexity

Clean Code Applied to JavaScript — Part VI. Avoid Conditional Complexity

carlillo profile image Carlos Caballero Originally published at carloscaballero.io ・5 min read

Introduction

Conditional complexity causes 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:

Discussion

pic
Editor guide
Collapse
jdmg94 profile image
José Muñoz

Very good patterns, the only thing that I would like to add is that the null-object pattern is rendered obsolete with conditional chaining, i.e: animal?.sound() will return null if the object is null or if it doesn't have the property, IMO it keeps things DRY. Polymorphism is a concept from OO, this and the last pattern can be both solved with object literals (which is already a part of the command pattern), consider the following:

function logMessage(message = "CRITICAL::The system ...") {
    const [level, messageLog] = message.split("::");
    const handler = param => {
        NOTICE: `notice: ${param}`
        CRITICAL: `critical: ${param}`
        CATASTROPHE: `catastrophe: ${param}`
    }[level]

    console.log(handler(messageLog))
}

the same could be applied to polymorphism but I don't know where the type value is coming from, maybe if you're already using objects then polymorphism is the right choice, however mostly any switch can be replace with an object literal, in conjunction with optional chaining you could safely have a switch that doesn't break when you pass an invalid type, i.e:

Auto.prototype.getProperty = () => {
  const handler = {
    BIKE: this.getBaseProperty
    CAR: () => this.getBaseProperty() - this.getLoadFactor()
    BUS: () => this.isNailed ? 0 : this.getBaseProperty(this.voltage)
  }[this.type]

  return handler?.()
}

but then again, if you're using classes you might want its own separate entity instead like illustrated in the article, I have found that its not very common in JS since classes tend to be verbose, these are only my opinions based on anecdotal evidence of course, great series!

Collapse
swarupkm profile image
Swarup Kumar Mahapatra

I think the whole intent of this article is "let the client code/ calling code do the if/switch and pass on messages"...
Conditionals can be delegated IMO, but cannot be eliminated.