I went to an OOP workshop by Sandi Metz several years ago. She made a comment that at one of her previous jobs, they didn't use
if
statements.What?
I had no idea how that was even possible.
I recently got interested in this idea again while refreshing my knowledge of design patterns. That's when I began to come across things like the anti-IF campaign and "Death to the IF statement."
At this point, I didn't know if coding without conditionals was really a good idea, but I saw that at least there were other presumably intelligent people who've thought about it and had strong opinions.
Assuming for a second that you can code without
if
statements, why should you?The guiding idea behind coding without
if
statements is the Open/Closed Principle (the "O" from SOLID).From Wikipedia:
In object-oriented programming, the open/closed principle states "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification";[1] that is, such an entity can allow its behaviour to be extended without modifying its source code.
The last line is the important one.
To follow the Open/Closed principle, you've got to be able to add new functionality to an entity (or class) without changing its source code.
When you don't follow this principle, you end up with conditional spaghetti monsters.
Why?
Because as requirements change or get added, you can always just tack on an extra
if
. This can lead to wildly complex code that's next to impossible to change.So how can you do it?
Let's look at a quick example. First, keep in mind that this is a trivial example, and it might not actually be a good candidate in a real work environment for this kind of refactoring. (It's also just using
puts
instead of doing anything real.)But, to keep this somewhat short, we need a simple example. So here goes.
Here's a bit of code about paying an employee in Ruby. Essentially it's simulating sending out monthly payments based on either your salary or the number of hours you've worked.
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 we want to pull the
if
statement out of the method, we can refactor this using the Strategy Pattern.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)
Since we're using Ruby, now 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))
If we assume that the
type
attribute is coming from the database (or similar), we can now add new payment strategies without ever modifying the Employee
class. We just need to tag people with the right type
in the database and then add a new strategy class.So, it follows the Open/Closed principle.
Nothing in engineering is free though, so what are the tradeoffs?
First of all, for simple situations, the new version is much more challenging to get your head around. There are several classes that are likely all in different files.
Before we made the change, we could read one simple paragraph and understand what was going on. Now we need to look at a system of small objects.
Personally, I prefer the small objects, but in reality, I'd leave the code alone until a new requirement was added.
I'm probably going to continue using conditionals in my code, but exercises like this are great to do so that you have fresh ideas while you're doing your real work.
It's also important to keep things like the Open/Closed principle in the back of your mind so you know when you are violating it and can decide when it's worth it make updates in order to adhere to it.