Some Best Practices in Ruby on Rails

  • Use ‘each’ instead of ‘for’
for elem in [1, 2, 3, 4, 5]
puts elem
end

[1, 2, 3, 4, 5].each { |elem| puts elem }
  • Use ‘Case/when’ conditions instead of lengthy “if” statements
puts "What is your task status?"
status= gets.chomp

case status
when ‘inprogress’
 puts "The task is in progress. Rapidly working on it."
when ‘hold’
 puts "The task is in hold. Will work on it after client's feedback."
when ‘done’
 puts "The task has been completed. Got a good response from the client."
else
 puts "The task is in TODO."
end
  • Use double bangs(!!) to determine if the value exists
@middle_name #=> nil
!@middle_name #=> true
!!@middle_name #=> false
  • Avoid n+1 query

Rails has a (in)famous query problem known as the n+1 query problem i.e eager loading.

Take the following case where a user has a house:

class User < ActiveRecord::Base
   has_one :house
end

class House < ActiveRecord::Base
   belongs_to :user
end

Bad Practice

A common mistake made while retrieving the address for house of each user is to do the following in the controller:

@users = User.limit(50)


<% @users.each do |user|%>
    <%= user.house.address %>
<% end %>

The above code will execute 51 queries, 1 to fetch all users and other 50 to fetch the house of each user.

Good Practice

The retrieval should be made as follows

@users = User.includes(:house).limit(50)

<% @users.each do |user|%>
    <%= user.house.address %>
<% end %>

The above code will execute 2 queries, 1 to fetch all users and other to fetch house for the user.

Bullet – A gem that warns of N+1 select (and other) issues

  • Follow the law of Demeter

According to the law of Demeter, a model should only talk to its immediate associated models. If you want to use associated attributes then you should use ‘delegate’.

Using this paradigm, a model delegates its responsibility to the associated object.

Bad Practice

class Project < ActiveRecord::Base
    belongs_to :creator
end

<%= @project.creator.name %>
<%= @project.creator.company %>

Good Practice

class Project > ActiveRecord::Base
belongs_to :creator
    delegate :name, :company, :to => :creator, :prefix => true
end

<%= @project.creator_name %>
<%= @project.creator_company %>
  • Use ? for the method that returns boolean value

Another convention is to use ? at the end of a method returning a boolean value.

Bad Practice

def exist
end

Good Practice

def exist?
end
  • Don't put too much logic in views. Make a helper method for views.

Bad practice

<% if book.published? && book.published_at > 1.weeks.ago %>
  <span>Recently added</span>
<% end %>

Good practice

# app/view/helpers/application_helper.rb
module ApplicationHelper
  def recently_added?(book)
    book.published? && book.published_at > 1.weeks.ago
  end
end


<% if recently_added?(book) %>
  <span>Recently added</span>
<% end %>
  • Use Local Variables in Place of Instance Variables in Partials

The purpose of a partial view is to reuse it anywhere but if we use instance variable in partials, it could lead to conflicting results making it hard to reuse. A better approach is to use local variables in partial views.

Bad Practice

<%= render :partial => 'header' %>

Good Practice

<%= render :partial =>  'header', :locals => {:project => @project}%>
  • Fat Models, Skinny Controllers and Concerns

Another best practice is to keep non-response related logic out of the controllers. Examples of code you don’t want in a controller are any business logic or persistence/model changing logic. For example, someone might have their controller like:

class BooksController < ApplicationController
  before_action :set_book, only: [:show, :edit, :update, :destroy, :publish]

  # code omitted for brevity

  def publish
    @book.published = true
    pub_date = params[:publish_date]
    if pub_date
      @book.published_at = pub_date
    else
      @book.published_at = Time.zone.now
    end

    if @book.save
      # success response, some redirect with a flash notice
    else
      # failure response, some redirect with a flash alert
    end
  end

  # code omitted for brevity

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_book
      @book = Book.find(params[:id])
    end
  # code omitted for brevity
end

Let’s move this complicated logic to the relevant model instead:

class Book < ActiveRecord::Base

  def publish(publish_date)
    self.published = true
    if publish_date
      self.published_at = publish_date
    else
      self.published_at = Time.zone.now
    end
    save
  end
end

class BooksController < ApplicationController
  before_action :set_book, only: [:show, :edit, :update, :destroy, :publish]

  # code omitted for brevity

  def publish
    pub_date = params[:publish_date]
    if @book.publish(pub_date)
      # success response, some redirect with a flash notice
    else
      # failure response, some redirect with a flash alert
    end
  end

  # code omitted for brevity

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_book
      @book = Book.find(params[:id])
    end
  # code omitted for brevity
end

This was a straightforward case where it is clear that this piece of functionality belongs to the model. There are lots of other cases where you have to be smart to find the right balance and know what should go where. Sometimes, logic you take out from a controller doesn’t fit into the context of any model. You have to figure out where would it fit the best. Here are some lists to see where the logic fits the best.

  • Controllers should only make simple queries to the model. Complex queries should be moved out to models and broken out in reusable scopes. Controllers should mostly contain request handling and response related logic.

class PostsController < ApplicationController
  def index
    @posts = Post.where(status: "Published").where(created_at: Time.zone.now.at_beginning_of_week...Time.zone.now.at_end_of_week)
   end
 end


class Post < ApplicationRecord
  scope :published, -> { where(status: "Published") }
  scope :this_week, -> { where(created_at: Time.zone.now.at_beginning_of_week...Time.zone.now.at_end_of_week) }
end
class PostsController < ApplicationController
  def index
    @posts = Post.published.this_week
   end
 end
  • Any code that is not request and response related and is directly related to a model should be moved out to that model.

  • Any class which represents a data structure should go into the app/models directory as a Non-ActiveRecord model (table-less class).


class AbstractModel < ActiveRecord::Base
  def self.columns
    @columns ||= add_user_provided_columns([])
  end

  def self.table_exists?
    false
  end

  def persisted?
    false
  end
end

class Market::ContractorSearch < AbstractModel
  attribute :keywords,           Type::Text.new,    :default => nil
  attribute :rating,             Type::Text.new,    :default => [].to_yaml
  attribute :city,               Type::String.new,  :default => nil
  attribute :state_province_id,  Type::Integer.new, :default => nil
  attribute :contracted,         Type::Boolean.new, :default => false

  serialize :rating

  belongs_to :state_province

  has_many :categories, :class_name => 'Market::Category'
  has_many :expertises, :class_name => 'Market::Expertise'
end
  • Use PORO (Plain Old Ruby Objects) Ruby classes when logic is of a specific domain (Printing, Library & etc.) and doesn’t really fit the context of a model (ActiveRecord or Non-ActiveRecord). You can put those classes in app/models/some_directory/. Anything inside the app/ directory is automatically loaded on app startup as it’s include in the Rails autoload path. POROs can also be placed in app/models/concerns & app/controllers/concerns directories.
module Printable
 include ActiveSupport::Concern
 def print(format, content)
   raise UnknownFormatError unless ['pdf', 'doc'].include?(format)
   # do print content
 end
end

class Document
 include Printable
 def initialize(format, content)
   @format = format
   @content = content
 end
 def export
   # ...
   print(@format, @content)
 end
end
  • Place your PORO, Modules, or Classes in lib/ directory if they are application independent and can be used with other applications as well.

  • Use modules if you have to extract out common functionality from otherwise unrelated functionality. You can place them in app/* directory and in lib/ directory if they are application independent.

  • The “Service” layer is another really important place in supporting vanilla MVC when the application code is growing and it’s getting hard to decide where to put specific logic. Imagine you need to have a mechanism to send SMS or Email notifications to some subscribers when a book is published, or a push notification to their devices. You can create a Notification service in app/services/ and start service-ifying your functionality.

To add a member to klaviyo list, create a file named klaviyo_service.rb in app/services

require 'faraday'

class KlaviyoService

 def add_member_to_list(new_email,domain)
   url = "https://a.klaviyo.com/api/v2/list/#{ENV['KLAVIYO_LIST_ID']}/members"
   response = Faraday.post(url) do |req|
     req.headers['Content-Type'] = 'application/json'
     req.headers['api-key'] = "#{ENV['KLAVIYO_PRIVATE_API_KEY']}"
     properties = { "profiles":
                   [
                     {
                       "email": "#{new_email}",
                       "domain": "#{domain}",
                       "domain_status": "installed"
                     }
                   ]
                 }.to_json
     req.body = "#{properties}"
   end
   puts "Added #{new_email} to klaviyo list"
 end
end

Call the service wherever required:

  KlaviyoService.new.add_member_to_list(“xyz@gurzu.com”, “gurzu.com”)