- controllers should be responsible for orchestrating the models. means that you should always tell objects what to do, and not ask them question about there state
# controller
if @item.publish
flash[:notice] = "xxxx"
end
# modle
class Item < ActiveRecord::Base
def publish
if user == "username"
return false
end
self.published_on = Time.now
self.save
end
end
- callbacks should only be for modifying internal state and should avoid calling other domain objects
class User < ActiveRecord::Base
before_create :set_name, :set_token
protected
# right
def set_name
self.name = self.login.capitalize if name.blank?
end
# wrong
def set_toke
self.token = TokenGenerator.create(self)
end
end
- encapsulate unique business logic out of your AR models by using PORO
class UserSuspension
def initialize(user)
@user = user
end
def create
@user.class.transaction do
disapprove_user!
disapprove_comments!
end
end
private
def disapprove_user!
disapprove_comments!
end
- extracting queries to class method class or scope
class Post < ActiveRecord::Base
def self.recent
where("published = ? AND published_on > ?", true, 2.days.ago)
end
# or
scope :recent, -> { where("published = ? AND published_on > ?", true, 2.days.ago) }
end
- use scope to always return a chainable object, this is better than class method
class Post < ActiveRecord::Base
scope :by_author, ->(author) { where(author: author) if author.present? }
scope :recent, -> { where('published_on > ?', 2.days.ago) }
end
# this is very useful when doing chain query even author is 'nil'
# scope will automatically ignore the nil scope
Post.by_author(nil).recent
- rails 4 default use append(rails 3 use override) when chain scopes with same attributes, you should use merge to approve override logic
scope :active, -> { where(state: 'active') }
scope :inactive, -> { where(state: 'inactive') }
User.active.inactive # select * from users where state = 'active' and state = 'inactive'
User.active.merge(User.inactive) # select * from users where state = 'inactive'
help extract duplicate code into reusable modules that can be mixed into multiple controllers or models
- move shared model code into model concerns
module Commentable
extend ActiveSupport::Concern
included do
has_many :comments, as: :commentable
end
def comments_by_user(id)
comments.where(user_id: id)
end
end
# use
class Post < ActiveRecord::Base
include Commentable
end
class Image < ActiveRecord::Base
include Commentable
end
- move shared controller code into controller
module Previewable
def thumbnail(attachment)
file_name = File.basename(attachment.path)
"/thumbs/#{file_name}"
end
end
class ImageController < ApplicationController
include Previewable
def show
@image = Image.find(params[:id])
@thumbnail = thumbnail(@image)
end
end
class VideoController < ApplicationController
include Previewable
def show
@video = Image.find(params[:id])
@thumbnail = thumbnail(@video)
end
end
- wrapped the modle for handling the view logic, you should define method_missing and respond_to_missing? methods for delegating method to the object
class PostDecorator
attr_reader :post
def initialize(post)
@post = post
end
def is_front_page?
post.published_at > 2.days.ago
end
def publication_date
post.created_at.strftime '%Y-%m-%d'
end
def method_missing(method_name, *args, &block)
post.send(method_name, *args, &block)
end
def respond_to_missing?(method_name, include_private=false)
post.respond_to?(method_name) || super
end
end
# use
<span><%= @post_decorator.publication_date %></span>
cool