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 objectsTo 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 endNow 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 endDesigning 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 helpersPartials 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 endAnd 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.