Blog Post
Published on Apr 18, 2020

Coding Without if Statements

Several years ago, I went to a workshop with Sandi Metz.

First of all, it was amazing, and if you ever get the chance, you should attend one.

Sandi said something kind of puzzling while we were all together. She talked about "coding without if statements."

If you're like me, this is a really strange idea.

Conditionals are one of the first things you learn when you start coding. They are all over the place. How could you possibly get rid of them?

Over on our Youtube channel, we're building out a series on Design Patterns in Javascript. In one of the lasest episodes, I worked through the strategy pattern. In the process, I remembered what Sandi said and thought it would be an interesting exercise to try to start with a chunk of code with a conditional right in the middle and see if I can totally remove it.

So, let's give it shot.

Here's a bit of code about paying an employee in Ruby. It's got a conditional in the #send_payment method that we want to eliminate.

class Employee

  attr_reader :name, :payment_details

  def initialize(name, payment_details = {})
    @name = name
    @payment_details = payment_details
  end

  def send_payment
    if !payment_details[:hourly_rate].nil?
      hourly_rate = payment_details[:hourly_rate].to_f
      number_of_hours = payment_details[:number_of_hours]
      amount = (hourly_rate * number_of_hours).round(2)
      puts "Sending $#{amount} to #{name}"
    else
      amount = (payment_details[:salary].to_f / 12).round(2)
      puts "Sending $#{amount} to #{name}"
    end
  end

end

jennifer = Employee.new("Jennifer Smith", { salary: 135000 })
jennifer.send_payment

max = Employee.new("Max Baxter", { hourly_rate: 92.50, number_of_hours: 122 })
max.send_payment

If you want to see the initial refactoring, check out the Youtube video on the Strategy Pattern.

To summarize, we pull the different situations - hourly vs. salary - up into strategy classes. We'll end up with something like this in Ruby.

class SalaryStrategy

  attr_reader :payment_details

  def initialize(payment_details = {})
    @payment_details = payment_details
  end

  def amount
    (payment_details[:salary].to_f / 12).round(2)
  end

end

class HourlyStrategy

  attr_reader :payment_details

  def initialize(payment_details = {})
    @payment_details = payment_details
  end

  def amount
    hourly_rate = payment_details[:hourly_rate].to_f
    number_of_hours = payment_details[:number_of_hours]
    amount = (hourly_rate * number_of_hours).round(2)
  end

end

class Employee

  attr_reader :name, :payment_details

  def initialize(name, payment_details = {})
    @name = name
    @payment_details = payment_details
  end

  def send_payment(strategy_class)
    strategy = strategy_class.new(payment_details)
    amount = strategy.amount
    puts "Sending $#{amount} to #{name}"
  end

end

jennifer = Employee.new("Jennifer Smith", { salary: 135000 })
jennifer.send_payment(SalaryStrategy)

max = Employee.new("Max Baxter", { hourly_rate: 92.50, number_of_hours: 122 })
max.send_payment(HourlyStrategy)

As you can see, we're injecting the strategy into the method to send payment, and now our if statement is gone.

Well, sort of...

Deep down, we all know that up in our controller (or wherever) we'd have something like this:

class PaymentsController < ApplicationController
  ...
  def run
    if @employee.hourly?
      @employee.send_payment(HourlyStrategy)
    else
      @employee.send_payment(SalaryStrategy)
    end
  end
  ...
end

We could refactor that to make it look less duplicated, but in the end, there would still be an if statement buried in there.

So how can we truly get rid of that if statement?

In my example, there is some execution code at the bottom. It creates a couple of employees and them pays them. Let's update that to contain a type attribute as well.

payment_details = { salary: 135000, type: "salary" }
jennifer = Employee.new("Jennifer Smith", payment_details)
jennifer.send_payment(SalaryStrategy)

payment_details = { hourly_rate: 92.50, number_of_hours: 122, type: "hourly" }
max = Employee.new("Max Baxter", payment_details)
max.send_payment(HourlyStrategy)

Next, since we're using Ruby and it's got the magic, we can do something like this:

class StrategyFactory

  def self.for(payment_details)
    Module.const_get("#{payment_details[:type].capitalize}Strategy")
  end

end

Now, we can refactor our execution code to just use the factory.

payment_details = { salary: 135000, type: "salary" }
jennifer = Employee.new("Jennifer Smith", payment_details)
jennifer.send_payment(StrategyFactory.for(payment_details))

payment_details = { hourly_rate: 92.50, number_of_hours: 122, type: "hourly" }
max = Employee.new("Max Baxter", payment_details)
max.send_payment(StrategyFactory.for(payment_details))

Now, we've completely eliminated that if statement (and introduced a naming convention).

So... can you always do that?

To be honest, I have no idea.

Is it worth it?

I'm not sure about that either.

However, it is a really interesting exercise.

As a side note, it's interesting that if you start with the code we had at the beginning and just try to iteratively remove the if statement, you end up getting into the Gang of Four patterns pretty quickly.