- 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”)