if (!_if) what


From time to time, the use of if statements causes a bit of debate in my computing circles (it's funny to hear us start arguments with "if you use if..."). Most recently, I came across this post. In one of the comments, an assertion was made that if statements should be avoided since they represent design flaws. While I don't agree that the existence of if statements in code are all bad, I was inspired to share a few instances where I tend to avoid using them. This article focuses on JavaScript, but most of the concepts presented are language-neutral.

The debated example

In the comments of the aforementioned article, many of us started rewriting the following example if...else block.

const wow = arg => {

  if(arg === "dog"){
    return "LOVELY";
  } else if(arg === "cat"){
    return "CUTE";
  } else {
    return ("gimme an animal");
  }
}

wow("cat");  
//-> "CUTE"

While the example was fine for demonstrating the author's point (we picked it apart anyway because we'll rip apart everything but our paychecks), it does present a few opportunities for improvement.

Else if, else if, else if

The first issue is that whenever a new condition is needed, a new else if clause must be added. So if you wanted to say "AWESOME" in response to "pony", you'd need to adjust the code as follows:

const wow = arg => {

  if(arg === "dog"){
    return "LOVELY";
  } else if(arg === "cat"){
    return "CUTE";
  } else if(arg === "pony"){
    return "AWESOME";
  } else {
    return ("gimme an animal");
  }
}

wow("pony");  
//-> "AWESOME"

This would be repeated for every new animal and makes for very brittle, difficult to test, code.

The conditionals

Rather than using so many if...else if blocks, one could rewrite the function with conditional statements. Here is a comment from the linked article demonstrating this approach:

const wow = arg => (  
  (arg === "dog" && "LOVELY") ||
  (arg === "cat" && "CUTE") ||
  "gimme an animal"
);

wow("cat");  

There are no if statements present, but you are still left with the original maintenance issue. That is, you'd need to add an additional condition for each new animal.

The Data Map

One way to eliminate this growing set of else if statements is to store your relationships in a map. Consider the following:

const animals = {  
  dog: "LOVELY",
  cat: "CUTE",
  pony: "AWESOME",
};

const wow = arg => {  
  return animals.hasOwnProperty(arg) && animals[arg] || "gimme an animal";
};

wow("pony");  
//-> "AWESOME"

Here, we've replaced the if...else statement with a lookup in a data map. With this, we have drastically simplified the wow function and we no longer need to modify it when a new animal comes along.

Before continuing, I'd like to point out that removing if statements is not the point here. The point is to make your code less brittle and easier to maintain. The latest iteration of this example could just as well have been written as follows:

const animals = {  
  dog: "LOVELY",
  cat: "CUTE",
  pony: "AWESOME",
};

const wow = arg => {  
  if(animals.hasOwnProperty(arg)){ //WTF if, who invited you?
    return animals[arg];
  }
  return "gimme an animal";
};

wow("pony");  
//-> "AWESOME"

Going further...

You might look at the above and declare "But I still have to change the code! What's the difference?" I wouldn't fault you for that. So in this section, I will do a bit of restructuring in order to drive the point home.

First, let's abstract out the data.

//file: data.js

let animals;

//Let's pretend this is really being loaded from the database
//Let's also pretend the load is synchronous so we don't have
//get into a discussion of async/await or the Promise api
const loadAnimals = () => {  
  animals = {
    dog: "LOVELY",
    cat: "CUTE",
    pony: "AWESOME",
  };
};

const getAnimals = () => {  
  if(!animals) loadAnimals();
  return animals;
};

export default getAnimals;  

In this module, we are faking a database. The public getAnimals method will return the data from our datasource. Remember, the entire animals structure lives in the database, so modifications to it would happen there rather than in this file. For the sake of this discussion, let's pretend that data.js is the database.

Next, we implement our wow module.

//file: wow.js

import getAnimals from 'data';

const wow = name => {  
  const animals = getAnimals();
  return animals.hasOwnProperty(name) && animals[name] || "I'm sorry Dave, I'm afraid I can't do that";
};

export default wow;  

Notice here we import the data module and use it to grab the animals structure. Then, just as before, we either return the greeting (if one is present) or the silly string if no animal is found that matches the specified name.

The important point is that even if the set of animals changes or the greeting for each animal changes, this module does not need to be modified. That makes it much more maintainable since modifying or adding animals becomes an issue of data entry rather than a coding change. Your unit tests are greatly simplified because you need not test a branch per animal. In fact, you'd get 100% code coverage in this unit with just the following two tests.

  • should accept a name and return a greeting for the specified animal.
  • should return I'm sorry Dave, I'm afraid I can't do that if no animal matches; because all error messages should sound like a computer that sounds like a human trying to sound like a computer that sounds human.

Finally, you'd import and use this module from somewhere (here we'll just use index.js).

//file: index.js

import wow from 'wow';

wow('pony'); //-> AWESOME  
wow('horse') //-> gimme an animal  

Conclusion

Look, I'm not here to tell anyone how to code. I don't believe there is anything fundamentally wrong with using if statements. I absolutely don't believe in absolutes. I'm sure that last sentence harmed the same cat Schrödinger locked in that box. Did he ever answer to PETA for his actions?

Anyway, based on the needs of your project and your ability to convince the coding zealots you work with to turn a blind eye, you can likely get away with stringing a few if...else if...else statements together and shipping it. However, there are alternatives that will enhance the stability and testability of your code. This article points to the tip of that particular iceberg. If there is interest, I'll look into writing more about this and exploring some other popular patterns that can help. If not, just tell me to go to that place where that guy's cat was half of the time. Hell. I'm talking about hell.

comments powered by Disqus