Refactoring in Rails

convention over configuration. Putting too much logic in the controller will eventually violate the single responsibility principle making future changes to the codebase difficult and error-prone. Refactoring the code helps for quality, clarity, and maintainability.

When should we refactor?

Tests usually run faster with well-isolated code. Slow running tests indicate the need of more sophisticated design. For example, each class should be concerned about one unique functionality. Also, models and controllers with too many lines of code needed to be refactored to make code DRY and clean. We can use best approaches of Object Oriented Programming to achieve this. Consider the following example of a controller where the whole logic for creation and deletion is concentrated.
class WebsitesController < ApplicationController
  def create
    create_website
    find_or_create_user
    fetch_rank
    redirect_to root_url
  end
  def destroy
    .........
  end
.............
  private
  def create_website
    @website = website.first_or_create
    CollectionWebsite.create(collection_id: collection_id, website_id: website.id)
    @website.fetch_meta_description
  end
  def find_or_create_user
    site_urls = user.websites.map(&:url)
    websites_count = user.websites.count
    user.update!(sites: site_urls, site_number: websites_count)
  end
  def fetch_rank
    return if website.alexaranks.any?
    FetchRankJob.perform_later(website)
  end
 end
 

Moving a big controller’s action to service objects

To achieve the new design, keep controllers as thin as possible and always call service objects. Controllers are a good place to have the HTTP routing, parameters parsing, authentication, calling the right service, exception catching, response formatting, and returning the right HTTP status code. A service object’s job is to hold the code for a particular bit of business logic. Calling services from controllers result in many classes each of which serves a single purpose. This helps to achieve more isolation between objects. Another important thing is that service objects should be well named to show what an application does. Extracting code from controllers/models to service objects would support single responsibility principle, which leads to better design and better unit tests. The above example can be rewritten by calling separate services for creating and deleting as shown below. This considerably reduces the number of lines of code in the controller and provides more clarity on the tasks performed.
class WebsitesController < ApplicationController
  def create
    Websites::Create.call(website_params, current_user)
    redirect_to root_url
  end
  ..........
  def destroy
    Websites::Delete.call(website_params, current_user)
    redirect_to root_url
  end
  ..........
  private
  def website_params
    params.require(:website).permit(:url, :collection_id)
  end
end
Now WebsitesController looks cleaner. Service objects are callable from anywhere, like from controllers as well as other service objects, DelayedJob / Rescue / Sidekiq Jobs, Rake tasks, console, etc. In app/services folder, we create services for each controller’s actions. Prefer subdirectories for business-logic heavy domains.  For example, the file app/services/websites/create.rb will define Websites::Create while app/services/websites/delete.rb will define Websites::Delete.
module Websites
  class Create
    # From the controller, use it like this:
    # Websites::Create.call(params, user)
    def self.call(params, user)
      new(params, user).call
    end
    def initialize(params, user)
      @params = params
      @user = user
      @website = Website.where(url: params[:website][:url])
    end
    def call
      create_website
      find_or_create_user
      fetch_rank
    end
    private
    attr_reader :params, :user, :website
    def create_website
      @website = website.first_or_create
      CollectionWebsite.create(collection_id: collection_id, website_id: website.id)
      @website.fetch_meta_description
    end
    def find_or_create_user
      site_urls = user.websites.map(&:url)
      websites_count = user.websites.count
      user.update!(sites: site_urls, site_number: websites_count)
    end
    def fetch_rank
      return if website.alexaranks.any?
      FetchRankJob.perform_later(website)
    end
  end
end
Designing the class for a service object is relatively straightforward, since it needs no special gems and relies on the software design skills only. When the action is complex or needs to interact with an external service, service objects are really beneficial.

While refactoring, at each step we have to make sure that none of our tests failed.

Partials and helpers

Partials and helpers are the standard methods to extract reusable functionality. For larger HTML code, partials can be used to split into smaller logic parts. Partials are used for side-menu, header etc. When developing an application for the first time, I did notice the app/helpers directory but couldn’t find any use at that time. Generally, helpers are used for chunks of ruby code with minimal HTML or generating simple HTML from parameters. For example,  Once started using it, found it efficient in scenarios where we want to extract some complexity out of view and also if we want to reuse it or want to avoid it one day. This refers mostly to cases like conditionals or calculations. Consider something like this in the view:
<% if @user && @user.post.present? %>
  <%= @user.post %>
<% end %>
If put it in a helper,
module SiteHelper
  def user_post(user)
    user.post if user && user.post.present?
  end
end
And then in the view code, call the helper method and pass it the user as an argument.
<%= user_post(@user) %>
Views are in charge of displaying information only. They are not responsible for deciding what to display. The concept of object-oriented programming paved the way for design patterns and refactoring patterns such as service objects and also decorators and presenters. Decorator patterns enable us to attach additional responsibilities to an object dynamically, without affecting other objects of same class. A presenter is a type of subpattern of the decorator pattern. The main difference between them is how they extract logic out of the view. Presenters are very close to the view layer, while decorators are some more broad concept. Anyway, try to avoid helpers and concerns as much as possible, if you want to make objects easier to test, then Plain Ruby Objects are easier to test than helpers and concerns which are tied to Rails. The presenter pattern also has the problem of making things harder to test. Because of this, I prefer to use a mix of POROs (Plain Old Ruby objects) and helpers if can’t avoid having them. Always keep the minimum amount of code in helpers and move as much logic as possible into POROs. It’s the same with concerns – try to avoid them maximum, but it’s being used in larger code bases.  

References

    ]]>