Matt Moore

C++

Ruby » Should I Use if Statements? (Or Polymorphism to The Rescue)

Apr 18, 2017

I have been asked a lot lately about whether good programmers don't use if statements. I find this question amusing because it seems that those who ask about it almost have a fear of if statements. I thought I'd address this concern by talking about good design over a few blog posts, of which this is the first.

First, there's nothing wrong with a good old-fashioned if statement. It's OK to use them. There are times when overthinking and over-engineering can turn otherwise simple and easy-to-read code into an unmaintainable mess. Good design is about keeping things readable, maintainable, and avoiding unnecessary complexity.

That being said, there are a few basic principles that good code tends to follow. First, code should be broken down into functional and modular units. Classes and methods should be short, succinct, and precise. They should be easy to understand. Sandy Metz, well-known in the Ruby community, has a guideline that I like: classes should be 100 lines of code or less, and each method should be no more than 5 lines. That means you won't have a lot of room for complex if statements. Also, we should avoid "arrowhead" programming. This refers to the nesting of statements such that the indentation of the code resembles an arrowhead. For example:

def figure_something_out
  if some_condition?
    if some_other_condition?
      if a_third_thing_is_true?
      end
    end
  end
end

As you can see, we're nesting more and more if statements in that code. Each nested if statement starts to resemble an arrowhead pointing right. This is terrible. What if we have even more conditions to check for? Those nested if statements will begin to get unwieldy. Additionally, with each if statement that we're checking, we introduce yet another branch of logic that our method can fall into, and another condition that needs to be tested in the context of this method. Each additional condition creates what is referred to as high cyclomatic complexity. We want our methods to have low cyclomatic complexity. They should, as much as is possible, do one thing and do it well. It should be clear what the method is doing after reading the code for no more than 5 seconds.

This brings me to polymorphism. What is polymorphism? If you're familiar with object-oriented programming, you've seen this term before and may know what it is. If you're new to OOP, you may not have seen it yet. Polymorphism makes use of a concept in OOP languages known as the type system. In OOP, we create classes to represent the types of things we're programming for and we use the type system to make decisions for us that, in many cases, can eliminate the need for lots of if statements that often results from beginners who aren't familiar with the type paradigm.

Let's say I have a class called Animal, and that class has a method called move():

class Animal
  attr_reader :type

  def initialize(type)
    @type = type
  end

  def move
    if @type == :dog
      walk
    elsif @type == :fish
      swim
    end
  end

  private

  def walk
    puts "walking"
  end

  def swim
    puts "swimming"
  end
end

Now, to use this class:

dog = Animal.new(:dog)
=> #<Animal:0x007fcfdd8a4030 @type=:dog>
dog.move
=> walking

As you can see, the console will print "walking". It's pretty obvious why. It's because our if statements in move() determine what type of action the animal needs to perform in order to actually move. But, while this works, it's not very clean and doesn't make use of the type system. What if we have 1,000 animals that need to move in 1,000 different ways? This solution will not scale very well. We'd end up having to have 1,000 different if statements. The move() method would get very long.

Instead, we can make use of subclassing and rely on the type system to do its thing. This is what we mean when we say "polymorphism". Let's rewrite our animal class:

class Animal
  def type
    self.class
  end

  def move
  end
end

Pretty simple eh? But wait, it doesn't do anything now! That's OK. Let's add two more classes that inherit from Animal:

class Dog < Animal
  def move
    puts "walking"
  end
end
class Fish < Animal
  def move
    puts "swimming"
  end
end

Now, let's make use of these:

dog = Dog.new
=> #<Dog:0x007fa23d348848>
dog.type
=> Dog
dog.move
=> walking

fish = Fish.new
=> #<Fish:0x007fa23d29b9b8>
fish.type
=> Fish
fish.move
=> swimming

As you can see, we're now making use of the type system. Both are animals, and both share the type() method. They both also share the move() method, but their move() methods are different. We are getting these objects to respond to the same command of move() but they know that they move in different ways. We achieved this behavior without writing a single if statement.