Conditionals Aren’t Evil, Unless You Duplicate Them

We Hate If Statements

Many of us have been taught that, in OOP, most of a program’s “if” statements can be replaced by polymorphism. We talk about conditional statements as though they are a code smell, used only by programmers who haven't completely grasped polymorphism. This attitude is not hard to find:

At first, this attitude makes sense. We have all written code with heavily-nested conditionals. This code can be basically impossible to read and change; it also tends to have too much knowledge about the system. By definition, if we don’t use conditionals, these if-nests can’t exist! By using polymorphism over conditionals, we separate responsibilities and spread knowledge throughout the system.

I think that we hate conditionals more than we should. Introducing polymorphism too soon will complicate your code, making it hard to understand and test.

A Simple Example

Let’s zoom in and examine a simple conditional. We have a system where an employee's salary is determined by his or her title. Do we need to refactor this code? Is it telling us to refactor away the conditional?

class Employee
  attr_accessor :title, :name

  def initialize(employee_attributes)
    @title = employee_attributes[:title]
    @name = employee_attributes[:name]
  end

  def hours
    if title == :programmer
      80
    elsif title == :manager
      40
    end
  end
end

class Thank
  def generate(employee_attributes)
    employee = Employee.new(employee_attributes)
    "Thank you #{employee.name} for your #{employee.hours} hours of hard work!"
  end
end

A First Attempt At Refactoring

Here’s one way to avoid the if:

class Employee
  attr_accessor :title, :name

  def initialize(employee_attributes)
    @title = employee_attributes[:title]
    @name = employee_attributes[:name]
  end

  def hours
    {programmer: 80, manager: 40}.fetch(title)
  end
end

This is less expressive; we have simply buried our “if” inside a method call. We have also introduced a data structure and method call which did not previously exist. Yuck.

Polymorphic Version

Let’s try again, this time by using polymorphism:

class Employee
  attr_accessor :title, :name

  def initialize(employee_attributes)
    @title = employee_attributes[:title]
    @name = employee_attributes[:name]
  end
end

class Manager < Employee
  def hours
    40
  end
end

class Programmer < Employee
  def hours
    80
  end
end

class Thank
  def generate(employee_attributes)
    employee = EmployeeFactory.new_employee(employee_attributes)
    "Thank you #{employee.name} for your #{employee.hours} hours of hard work!"
  end
end

class EmployeeFactory
  def self.new_employee(employee_attributes)
    if employee_attributes[:title] == :programmer
      return Programmer.new(employee_attributes)
    elsif employee_attributes[:title] == :manager
      return Manager.new(employee_attributes)
    end
  end
end

Uh oh, the code grew! A lot! Also, we still have a pesky if! We had to put a lot of effort into building the polymorphic employee object.

It looks like we have some design tradeoffs to investigate.

Benefits of this design:

  • Each of our Employee classes is extremely simple.
  • The magic values :manager and :programmer are encapsulated.
  • This code looks easy to extend.

Drawbacks of this design:

  • There are more concepts for the reader grasp.
  • This code is harder to debug. We have to confront the uncertainty of not knowing the type of our employee. Are we dealing with an instance of a Manager when we mean to be dealing with a Programmer?
  • A change to the signature of the hours method will result in one change per type of employee. As the number of Employee classes grows, the signature will become more difficult to change.
  • Unit testing these classes in isolation gives us less confidence that the system works as a whole: we have to worry about their interaction.

The overhead of this design clearly outweighs its benefits.

Asking for Polymorphism

This example is not terribly far from asking for a polymorphic approach. It does not take much to push it over the edge.

Let’s add another method to our Employee class:

class Employee
  attr_accessor :title, :name

  def initialize(employee_attributes)
    @title = employee_attributes[:title]
    @name = employee_attributes[:name]
  end

  def hours
    if title == :programmer
      80
    elsif title == :manager
      40
    end
  end

  def salary
    if title == :programmer
      80000
    elsif title == :manager
      200000
    end
  end
end

Alarm bells should be going off in your head. We have duplication! We also have magic symbols! We all know why this is terrible. A change to one of the duplicated conditionals means changing all of the others. This is likely to become a giant pain. In other words, the code is not “open to extension but closed to modification”.

In this case, refactoring to polymorphism is totally warranted.

Conclusions

As soon as we observed duplication in our solution, it gave us a reason to cope with the indirection introduced by polymorphism. In my opinion, postponing polymorphism is a special case of YAGNI. We decided that the non-polymorphic approach was not "the simplest thing that could possibly work" when we encountered duplication and magic values.

Consider the task of adding a third type of employee, who has both an hours property and a salary property. If we take the non-polymorphic approach, this will be a pain: we will have to undergo the error-prone process of duplicating changes to conditionals. If we take the polymorphic approach, however, we are only required to write a short class. In this respect, the factory justifies its own existence.

It is helpful to think of polymorphism primarily as a tool for reducing duplication; this tool simply happens to occasionally yield elegant abstractions. Instead of creating your classes to model an a priori hierarchy of concepts, use them as a natural way to swiftly clear up duplication.

Wai Lee Chin Feman, Software Craftsman

Wai Lee Chin Feman is a software craftsman, enthusiastic about mathematics, functional programming, and philosophy.