BinaryWebPark

Refactoring If Statements Out of Your Ruby Code

April 25, 2017

Refactoring If Statements Out of Your Ruby Code

As I kept writing more code, I found I didn’t like conditionals all that much, especially when they started to obscure the purpose of the code. A technique I found from this blog post is described below.

Ruby basics

Why would you want to refactor?

I wanted to refactor to clean things up and make it easier to add new features going forward. Too much conditional logic (and nested logic) forces me to spend more time reasoning about code than I want to.

When would you want to refactor?

It’s really a personal choice. In a startup environment where things move fast, I tend to refactor in small bits in order to make it just a bit easier for someone else going forward.

I also tend to feel more comfortable refactoring when there is a robust test suite surrounding the code. This way you’ll know if your refactoring breaks something.

Example: Reasons for Unsubscribing from an Email List Code

Here’s a somewhat contrived example of conditional code loosely based on something I saw in production:

@reason_recorder = []
['too many emails', 'no longer interested', 'other reason'].each do |reason|
  if reason == 'too many emails'
    @reason_recorder << "too many emails from you"
  end
  if reason == 'no longer interested'
    @reason_recorder << "no longer interested in this stuff"
  end
  if reason == 'other reason'
    @reason_recorder << "another reason"
  end
end

Step 1: Build a collection of reasons

We build a collection of UnsubscribeReason classes housed by an UnsubscriberReasons class.

class UnsubscribeReasons
  def self.all
    [TooManyEmails, NoLongerInterested, OtherReason]
  end
end

Step 2: Build a reason class for the reason classes in Step 1 to inherit from

We build the parent class for the collection of classes in Step 1.

class UnsubscribeReason
  attr_accessor :reason_props

  def initialize(reason_props)
    @reason_properties = reason_props
  end
end

Step 3: Build the actual UnsubscribeReason classes

Now we build the actual UnsubscribeReason classes. You’ll notice they have an introspection property hasreason?_ that expects an OpenStruct with a reasontypes_ property so we can check whether a specific “unsubscribe reason” such as too many emails exists.

class TooManyEmails < UnsubscribeReason
  def self.has_reason?(reason_props)
    reason_props[:reason_types].include?(:too_many_emails)
  end
end

class NoLongerInterested < UnsubscribeReason
  def self.has_reason?(reason_props)
    reason_props[:reason_types].include?(:no_longer_interested)
  end
end

class OtherReason < UnsubscribeReason
  def self.has_reason?(reason_props)
    reason_props[:reason_types].include?(:other_reason)
  end
end

Step 4: Build a factory to fetch the reasons

Next we build a collection of UnsubscribeReasons that includes the classes that map to the various reasons a user unsubscribed.

class UnsubscribeReasonFactory
  def self.build_collection(reason_props)
    UnsubscribeReasons.all.select { |reason| reason.has_reason?(reason_props) }.map { |reason| reason.new(reason_props) }
  end
end

Step 5: Build a reasoner class to house the reasons

Finally, we build an UnsubscribeReasoner class with a reason list to house all the valid unsubscribe reason we found.

class UnsubscribeReasoner
  attr_accessor :reason_list

# use reason_list as the pivot point in the code that contains if statements
  def initialize
    @reason_list = []
  end

  def update_reason(reason_type:)
    reason_type = reason_type.name.underscore
    public_send("update_reason_with_#{reason_type}".to_sym)
  rescue NoMethodError
    nil
  end

  def update_reason_with_too_many_emails
    reason_list << "I received too many emails"
  end

  def update_reason_with_no_longer_interested
    reason_list << "I am no longer interested"
  end

  def update_reason_with_other_reason
    reason_list << "I have another reason"
  end
end

Step 6: Refactor the conditional code to use our new classes

reason_props = OpenStruct.new(short_desc: "", reason_types: [:too_many_emails, :no_longer_interested, :other_reason])
unsubscribe_reasons = UnsubscribeReasonFactory.build_collection(reason_props)
unsubscribe_reasoner = UnsubscribeReasoner.new
unsubscribe_reasons.each do |reason|
  unsubscribe_reasoner.update_reason(reason_type: reason.class)
end

p unsubscribe_reasoner.reason_list

# => ["I received too many emails", "I am no longer interested", "I have another reason"]

Maybe it’s a bit too contrived

The nice thing about the above refactoring is that if we want to add another unsubscribe reason, we more or less just have to add a class. The code is a bit longer, but I’ve been using contrived example code so saying the code is longer is a bit of an unfair statement.

You’ll have to try and see what works for you.

Summary

Overall, this is an attempt to show how you can refactor conditional logic out of your Ruby code. The example repository containing all this code is here.