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.