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.
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 has_reason? that expects an OpenStruct with a reason_types 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.