paint-brush
Can You Really Code Without IF Statements?by@stephenfiser
9,197 reads
9,197 reads

Can You Really Code Without IF Statements?

by StephenMay 2nd, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

The guiding idea behind coding without IF statements is the Open/Closed Principle (the "O" from SOLID) The 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. To follow the principle, you've got to be able to add new functionality to an entity (or class) without changing the source code. This can lead to wildly complex code that's next to impossible to change.

Company Mentioned

Mention Thumbnail
featured image - Can You Really Code Without IF Statements?
Stephen HackerNoon profile picture

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.

Conclusion

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.