DEV Community

Cover image for How to simplify your if statements
Taha Shashtari
Taha Shashtari

Posted on • Edited on • Originally published at tahazsh.com

How to simplify your if statements

Nested conditionals can make your code harder to read and change—especially if you nest them more than one level.

Below, I'll show an example of a function that returns a discount for the provided user.

  • It should give a 20% discount for all regular users.
  • But if the user has a premium membership, it will give a 40% discount.
  • Gold users take the highest discount: 60%.
  • If the user is banned, however, it should not give any discount.

Without guard clauses, I'll write this function like this:

function getDiscountForUser(user) {
  let result
  if (user.isBanned) {
    result = 0
  } else {
    if (user.isPremium) {
      result = 40
    } else {
      if (user.isGold) {
        result = 60
      } else {
        result = 20
      }
    }
  }
  return result
}
Enter fullscreen mode Exit fullscreen mode

I'll show you below how guard clauses can make it much simpler. But first, what are guard clauses?

What are Guard Clauses?

Guard clauses are the checks you put at the beginning of a function to check for the unusual conditions.

A usual (or called normal) condition is the condition that represents most cases for the needed behavior—in this case it's regular users getting a 20% discount. Anything other than that is called an unusual condition.

Guard clauses also return immediately if true—preventing other conditions from running.

Here's how the same function looks but with guard clauses:

function getDiscountForUser(user) {
  if (user.isBanned) return 0
  if (user.isPremium) return 40
  if (user.isGold) return 60

  return 20
}
Enter fullscreen mode Exit fullscreen mode

With the beauty of guard clauses, the function became way much easier to read and change.

Top comments (9)

Collapse
 
ant_f_dev profile image
Anthony Fung

I completely agree about using guard statements, and how they can simplify things. In this particular example though, the code could be made more readable without introducing them.

function getDiscountForUser(user) {
  let result

  if (user.isBanned) {
    result = 0
  } else if (user.isPremium) {
    result = 40
  } else if (user.isGold) {
    result = 60
  } else {
    result = 20
  }

  return result
}
Enter fullscreen mode Exit fullscreen mode

By specifying the default state upfront, things can be simplified further:

function getDiscountForUser(user) {
  let result = 20;

  if (user.isBanned) {
    result = 0
  } else if (user.isPremium) {
    result = 40
  } else if (user.isGold) {
    result = 60
  }

  return result
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
tahazsh profile image
Taha Shashtari • Edited

Thanks for your comment. Actually, there's nothing wrong with your example, but the main idea of guard clauses is to communicate the intention of my code by showing what part is the normal flow. The normal flow is the what the majority of the cases would be, in this example it's the regular user account.

With guard clauses, I can show that I'm handling the special cases first before going to the main thing I'm working on. I think I could've made it more clear in this example by showing that the normal flow would usually contain more logic.

function getDiscountForUser(user) {
  if (user.isBanned) return 0
  if (user.isPremium) return 40
  if (user.isGold) return 60

  // some long code that computes the discount amount for this specific user.
  // It might involve checking things like number of previous purchases and
  // if the user has used a discount code before.
  // And then return the result here
}
Enter fullscreen mode Exit fullscreen mode

It's usually found that the unusual cases can return immediately with some value; that's why I put them at the beginning. And after that I can write all the needed computation code for the normal flow of the function.

So, when I look at this code, I say: "Ah! I see that this function has some special cases, and it's handling them by returning a value immediately. Ok, that means I should see the main code below these ifs.".

Another reason I prefer guard clauses is that it helps me reduce the number of mutable data. You can see that in my example, there's no longer a result variable in the function. Mutable data can usually cause bugs and confusion because you need to make sure that other parts of the code didn't modify it in an unexpected way. In this particular example, it's not a big deal since its scope is very small, and the code is very simple to reason about. But the more complex your code is, the harder it would be to reason about.

Again, there's nothing wrong with your example, but I wanted to clarify why I chose guard clauses for this example and how they can help me view the intention of my code better. Thanks!

Collapse
 
ant_f_dev profile image
Anthony Fung

Thanks for your explanation. All good points that I agree with.

I regularly use guard statements, e.g. for checking user permissions on Web controller endpoints - if a user doesn't have permissions, return immediately. Another benefit is less indentation of code.

Agree with immutability too. I'll use const wherever I can, rather than let or var. This becomes one of the main reasons that I use the ternary operator ?. However, if the logic is complex or results in nested ternary operators, I'll opt for let to make the overall flow more readable and debuggable.

Collapse
 
tahazsh profile image
Taha Shashtari

Polymorphism can remove conditionals, but you have to be careful of overusing it. If your conditionals are very simple (like the example in this article), then polymorphism can make them more complicated instead of simplifying them.

I'll show you below how polymorphism can remove if statements, but in case you're not familiar with it, check out this article I wrote: What is Polymorphism?.

There are two cases where replacing conditionals with polymorphism would improve your code:

  • When you keep doing the same checks throughout your code. So, in the above example, if you find yourself repeating the same checks of user type (to see if they are gold, premium, or regular).
  • When you have some code with a very complex logic, and in that complex logic you have some if statements that determines the flow of that code. In this case, you can extract a base class for the main flow, and extract subclasses for each variation of that flow.

It's not easy to explain the second case in a comment; it requires its own post. So, you can ignore the second case for now, and let me show you an example of the first one.

Polymorphism works by having different implementations for the same interface (I explained that in details in the post I shared above). So, if I were to take the example in this post, I need to create a base class for user and a subclass for each user type.

class User {}

class PremiumUser extends User {}

class GoldUser extends User {}

class BannedUser extends User {}
Enter fullscreen mode Exit fullscreen mode

As I mentioned in the first point above, polymorphism is worth doing if you have repeated checks of the user type in many places of your code. But for simplicity, I'll only consider the function I'm doing the checks in: getDiscountForUser.

To unify the interface of the user objects, then I need to add getDiscountForUser to the User class—but let's rename it getDiscount.

Quick note: adding this method to the User class doesn't make sense because getting a discount is not the responsibility of the user model. But let's ignore that for demonstration purposes.

class User {
  getDiscount() {
    return 20
  }
}

class PremiumUser extends User {
  getDiscount() {
    return 40
  }
}

class GoldUser extends User {
  getDiscount() {
    return 60
  }
}

class BannedUser extends User {
  getDiscount() {
    return 0
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, you can replace all the ifs in getDiscountForUser with just user.getDiscount method call.

function getDiscountForUser(user) {
  return user.getDiscount()
}
Enter fullscreen mode Exit fullscreen mode

An important thing to note here (which you might be wondering about) is that there should be some factory function in your code that determines which user object to create. In the last code snippet above, I'm assuming that the user parameter is the correct user type. But that's determined in another place, usually a factory function, like this:

function createUser(userData) {
  switch(userData.type) {
    case 'gold':
      return new GoldUser(userData)
    case 'premium':
      return new PremiumUser(userData)
    case 'banned':
      return new BannedUser(userData)
    default:
      return new User(userData)
  }
}
Enter fullscreen mode Exit fullscreen mode

I hope you found this answer helpful to you.

Thread Thread
 
mcsee profile image
Maxi Contieri

I agree that polymorphism should not be abused but the last factory method also uses a case statement violating open closed principle.

With the aid of polymorphism you can convert createUser() to an open and extensible solution without all those hardcoding factory classes.

Thread Thread
 
tahazsh profile image
Taha Shashtari

The open-closed principle is a good one, but I wouldn't start applying all possible abstractions upfront. If you don't expect to have tens of subclasses that might grow in number in the future, a switch statement would be perfect.

After all, you should have tests for that part, and if you decided to store all the different subclasses in a map to remove the switch statement, then it should be easy to refactor to that.

Everyone has their own design style. For me I like YAGNI; so, I start with the simplest possible solution, and then improve the design incrementally as I see appropriate. By "the simplest possible solution" I mean the simplest abstractions that meet what I expect for a specific module to need in terms of architecture.

Thread Thread
 
mcsee profile image
Maxi Contieri

I think in this example, YAGNI and KISS will be the best solution.
Once you have good coverage and the business evolves you can do the refactor and remove the switch if necessary

Collapse
 
idleman profile image
idleman • Edited

A better alternative would probably be:

function getDiscountForUser(user) {
    return  user.isBanned ? 0 :
                user.isPremium ? 40 :
                user.isGold ? 60 :
                20;
  }
Enter fullscreen mode Exit fullscreen mode

It also guide the developer to think in terms of small functions instead of large ones.

Collapse
 
mcsee profile image
Maxi Contieri

you can improve this solution by removing all ifs with polymorphism