Imagine that you have the next controller action that works, but when you see it you feel that you can make it cleaner.
def index
if params[:term]
@hotels = Hotel.where(published: true).search(params[:term]).order("name ASC")
elsif params[:hotel_type]
if params[:stars]
@hotels = Hotel.where(published: true).where(hotel_type_id: params[:hotel_type].to_i).where(stars: params[:stars]).order("name ASC")
else
@hotels = Hotel.where(published: true).where(hotel_type_id: params[:hotel_type].to_i).order("name ASC")
end
else
@hotels = Hotel.where(published: true).order("name ASC")
end
end
There are different ways to try to refactor this code, but now I want to talk about how you can clean your code trying to be very explicit about the business rules that the code is implementing.
Watching the code I can find the next rules:
Now that we have the specification we can…
One implementation for this rules could be:
def index
if params[:term]
@hotels = Hotel.find_published_ordered_by_name_searching_for(params[:term])
else
query = {}
if params[:hotel_type]
query.merge(hotel_type_id: params[:hotel_type])
end
if params[:stars]
query.merge(stars: params[:stars])
end
@hotels = Hotel.find_published_ordered_by_name_with_query(query)
end
end
class Hotel < ApplicationRecord
def self.find_published_ordered_by_name_searching_for(term)
where(published: true).order(name: :asc).search(term)
end
def self.find_published_ordered_by_name_with_query(attrs)
where(published: true).order(name: :asc).where(attrs.select { |_k, v| v.present? })
end
end
Here I am trying to be very clear about the two branches that can take our code if the user provides a “term”. Also I am trying to be very explicit saying that we are always looking for “published hotels ordered by name”.
Now that the rules are a little more clear we can…
For example we can change the param key hotel_type
to be
hotel_type_id
in order to be able to remove the if
statements of the
second branch. And also we can extract some helper methods in the Hotel
class.
def index
@hotels =
if params[:term]
Hotel.find_published_ordered_by_name_searching_for(params[:term])
else
Hotel.find_published_ordered_by_name_with_query(params.slice(:hotel_type_id, :stars))
end
end
class Hotel < ApplicationRecord
scope :published, -> { where(published: true) }
scope :by_name, -> { order(name: :asc) }
scope :search, -> { ... }
def self.find_published_ordered_by_name_searching_for(term)
published.by_name.search(term)
end
def self.find_published_ordered_by_name_with_query(attrs)
published.by_name.where(attrs.select { |_k, v| v.present? })
end
end
Sometimes we try to make the code more “beautiful” but I think is more important to try to express the business rules in the clearest way possible.
I send an email each week, trying to share knowledge and fixes to common problems and struggles for ruby on rails developers, like How to fetch the latest-N-of-each record or How to test that an specific mail was sent or a Capybara cheatsheet. You can see more examples on Most recent posts or All post by topic.