You are on page 1of 99

Rails Best Practices

ihower@gmail.com

2009/10

As this slide writing, the current Rails version is 2.3.4

Who am I ?

a.k.a. ihower
http://ihower.tw
http://twitter.com/ihower
http://github.com/ihower
(Hsinchu, Taiwan)

Ruby Taiwan
http://ruby.tw

Agenda

Concept: Whats good code?


Move Code from Controller to Model
RESTful best practices
Model best practices
Controller best practices
View best practices

Warning! you should have testing before modify!

Best Practice Lesson 0:

Concepts

Why best practices?

Large & complicated application

Team & different coding style

Your code become...

(Rigidity)

(Needless Repetition)

(Opacity)

(Fragility)
(Immobility)
(Viscosity)
(Needless Complexity)

Agile Software Development: Principles, Patterns, and Practices

We need good code:

Whats Good code?

Readability
Flexibility
Effective
Maintainability
Consistency
Testability

So, What we can do?

Best Practice Lesson 1:

Move code from Controller to


Model
action code 15
http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model

1.Move finder to named_scope

Before

class PostsController < ApplicationController


def index
@public_posts = Post.find(:all, :conditions => { :state => 'public' },
:limit => 10,
:order => 'created_at desc')
@draft_posts

end
end

= Post.find(:all, :conditions => { :state => 'draft' },


:limit => 10,
:order => 'created_at desc')

1.Move finder to named_scope

After

class UsersController < ApplicationController


def index
@published_post = Post.published
@draft_post = Post.draft
end
end
class Post < ActiveRecord::Base
named_scope :published, :conditions => { :state => 'published' },
:limit => 10, :order => 'created_at desc')
named_scope :draft, :conditions => { :state => 'draft' },
:limit => 10, :order => 'created_at desc')
end

2. Use model association


class PostsController < ApplicationController
def create
@post = Post.new(params[:post])
@post.user_id = current_user.id
@post.save
end
end

Before

2. Use model association


class PostsController < ApplicationController
def create
@post = current_user.posts.build(params[:post])
@post.save
end
end
class User < ActiveRecord::Base
has_many :posts
end

After

3. Use scope access

class PostsController < ApplicationController


def edit
@post = Post.find(params[:id)
if @post.current_user != current_user
flash[:warning] = 'Access denied'
redirect_to posts_url
end
end
end

Before

3. Use scope access

After

class PostsController < ApplicationController


def edit
# raise RecordNotFound exception (404 error) if not found
@post = current_user.posts.find(params[:id)
end
end

4. Add model virtual attribute

Before

<% form_for @user do |f| %>


<%= text_filed_tag :full_name %>
<% end %>
class UsersController < ApplicationController
def create
@user = User.new(params[:user)
@user.first_name = params[:full_name].split(' ', 2).first
@user.last_name = params[:full_name].split(' ', 2).last
@user.save
end
end

4. Add model virtual attribute


class User < ActiveRecord::Base
def full_name
[first_name, last_name].join(' ')
end
def full_name=(name)
split = name.split(' ', 2)
self.first_name = split.first
self.last_name = split.last
end
end

example code from http://railscasts.com/episodes/16-virtual-attributes

After

After

<% form_for @user do |f| %>


<%= f.text_field :full_name %>
<% end %>
class UsersController < ApplicationController
def create
@user = User.create(params[:user)
end
end

example code from http://railscasts.com/episodes/16-virtual-attributes

5. Use model callback


<% form_for @post do |f| %>
<%= f.text_field :content %>
<%= check_box_tag 'auto_tagging' %>
<% end %>
class PostController < ApplicationController
def create
@post = Post.new(params[:post])
if params[:auto_tagging] == '1'
@post.tags = AsiaSearch.generate_tags(@post.content)
else
@post.tags = ""
end
@post.save
end
end

Before

5. Use model callback


class Post < ActiveRecord::Base
attr_accessor :auto_tagging
before_save :generate_taggings
private
def generate_taggings
return unless auto_tagging == '1'
self.tags = Asia.search(self.content)
end
end

After

After

<% form_for :note, ... do |f| %>


<%= f.text_field :content %>
<%= f.check_box :auto_tagging %>
<% end
class PostController < ApplicationController
def create
@post = Post.new(params[:post])
@post.save
end
end

6. Replace Complex Creation


with Factory Method
class InvoiceController < ApplicationController
def create
@invoice = Invoice.new(params[:invoice])
@invoice.address = current_user.address
@invoice.phone = current_user.phone
@invoice.vip = ( @invoice.amount > 1000 )
if Time.now.day > 15
@invoice.delivery_time = Time.now + 2.month
else
@invoice.delivery_time = Time.now + 1.month
end
@invoice.save
end
end

Before

6. Replace Complex Creation


with Factory Method
class Invoice < ActiveRecord::Base
def self.new_by_user(params, user)
invoice = self.new(params)
invoice.address = user.address
invoice.phone = user.phone
invoice.vip = ( invoice.amount > 1000 )
if Time.now.day > 15
invoice.delivery_time = Time.now + 2.month
else
invoice.delivery_time = Time.now + 1.month
end
end
end

After

After

class InvoiceController < ApplicationController


def create
@invoice = Invoice.new_by_user(params[:invoice], current_user)
@invoice.save
end
end

7. Move Model Logic into the


Model
class PostController < ApplicationController
def publish
@post = Post.find(params[:id])
@post.update_attribute(:is_published, true)
@post.approved_by = current_user
if @post.create_at > Time.now - 7.days
@post.popular = 100
else
@post.popular = 0
end
redirect_to post_url(@post)
end
end

Before

7. Move Model Logic into the


Model
class Post < ActiveRecord::Base
def publish
self.is_published = true
self.approved_by = current_user
if self.create_at > Time.now-7.days
self.popular = 100
else
self.popular = 0
end
end
end

After

After

class PostController < ApplicationController


def publish
@post = Post.find(params[:id])
@post.publish
redirect_to post_url(@post)
end
end

8. model.collection_model_ids
(many-to-many)
class User < ActiveRecord::Base
has_many :user_role_relationship
has_many :roles, :through => :user_role_relationship
end
class UserRoleRelationship < ActiveRecord::Base
belongs_to :user
belongs_to :role
end
class Role < ActiveRecord::Base
end

Before

<% form_for @user do |f| %>


<%= f.text_field :email %>
<% for role in Role.all %>
<%= check_box_tag 'role_id[]', role.id, @user.roles.include?(role) %>
<%= role.name %>
<% end %>
<% end %>
class User < ApplicationController
def update
@user = User.find(params[:id])
if @user.update_attributes(params[:user])
@user.roles.delete_all
(params[:role_id] || []).each { |i| @user.roles << Role.find(i) }
end
end
end

After

<% form_for @user do |f| %>


<% for role in Role.all %>
<%= check_box_tag 'user[role_ids][]', role.id, @user.roles.include?(role)
<%= role.name %>
<% end %>
<%= hidden_field_tag 'user[role_ids][]', '' %>
<% end %>
class User < ApplicationController
def update
@user = User.find(params[:id])
@user.update_attributes(params[:user])
# @user.role_ids = params[:user][:role_ids]
end
end

9. Nested Model Forms (one-to-one)


class Product < ActiveRecord::Base
has_one :detail
end
class Detail < ActiveRecord::Base
belongs_to :product
end
<% form_for :product do |f| %>
<%= f.text_field :title %>
<% fields_for :detail do |detail| %>
<%= detail.text_field :manufacturer %>
<% end %>
<% end %>

Before

Before

class Product < ApplicationController


def create
@product = Product.new(params[:product])
@details = Detail.new(params[:detail])
Product.transaction do
@product.save!
@details.product = @product
@details.save!
end
end
end

example code from Agile Web Development with Rails 3rd.

9. Nested Model Forms (one-to-one)


Rails 2.3 new feature
class Product < ActiveRecord::Base
has_one :detail
accepts_nested_attributes_for :detail
end
<% form_for :product do |f| %>
<%= f.text_field :title %>
<% f.fields_for :detail do |detail| %>
<%= detail.text_field :manufacturer %>
<% end %>
<% end

After

After

class Product < ApplicationController


def create
@product = Product.new(params[:product])
@product.save
end
end

10. Nested Model Forms (one-to-many)


class Project < ActiveRecord::Base
has_many :tasks
accepts_nested_attributes_for :tasks
end
class Task < ActiveRecord::Base
belongs_to :project
end
<% form_for @project do |f| %>
<%= f.text_field :name %>
<% f.fields_for :tasks do |tasks_form| %>
<%= tasks_form.text_field :name %>
<% end %>
<% end %>

Nested Model Forms


before Rails 2.3 ?

Ryan Batess series of railscasts on complex forms

http://railscasts.com/episodes/75-complex-forms-part-3

Recipe 13 in Advanced Rails Recipes book

Best Practice Lesson 2:

RESTful
RESTful conventions

Why RESTful?
RESTful help you to organize/name controllers, routes
and actions in standardization way

Before
class EventsController < ApplicationController

def index
end

def feeds
end

def white_member_list
end

def watch_list
end

def show
end

def add_comment
end

def black_member_list
end

def add_favorite
end

def create
end

def show_comment
end

def deny_user
end

def invite
end

def update
end

def destroy_comment
end

def allow_user
end

def join
end

def destroy
end

def edit_comment
end

def edit_managers
end

def leave
end

def approve_comment
end

def set_user_as_manager
end
def set_user_as_member
end

end

After

class EventsController < ApplicationController


def index; end
def show; end
end
class
def
def
def
end

CommentsControlers < ApplicationController


index; end
create; end
destroy; end

def FavoriteControllers < ApplicationController


def create; end
def destroy; end
end
class EventMembershipsControlers < ApplicationController
def create; end
def destroy; end
end

Before

1. Overuse route customizations


map.resources :posts, :member => { :comments
=> :get,
:create_comment
=> :post,
:update_comment
=> :post,
:delete_comment => :post }

After

1. Overuse route customizations


Find another resources
map.resources :posts do |post|
post.resources :comments
end

Suppose we has a event model...


class Event < ActiveRecord::Base
has_many :attendee
has_one :map
has_many :memberships
has_many :users, :through => :memberships
end

Can you answer how to design


your resources ?

manage event attendees (one-to-many)


manage event map (one-to-one)
manage event memberships (many-to-many)
operate event state: open or closed
search events
sorting events
event admin interface

Learn RESTful design


my slide about restful:
http://www.slideshare.net/ihower/practical-rails2-350619

2. Needless deep nesting

Before

: Never more than one level

map.resources :posts do |post|


post.resources :comments do |comment|
comment.resources :favorites
end
end

<%= link_to post_comment_favorite_path(@post, @comment, @favorite) %>

2. Needless deep nesting

After

: Never more than one level

map.resources :posts do |post|


post.resources :comments
end
map.resources :comments do |comment|
comment.resources :favorites
end

<%= link_to comment_favorite_path(@comment, @favorite) %>

3. Not use default route


map.resources :posts, :member => { :push => :post }
map.connect ':controller/:action/:id'
map.connect ':controller/:action/:id.:format'

Before

3. Not use default route


map.resources :posts, :member => { :push => :post }
#map.connect ':controller/:action/:id'
#map.connect ':controller/:action/:id.:format'
map.connect 'special/:action/:id', :controller => 'special'

After

Best Practice Lesson 3:

Model

Before

1. Keep Finders on Their Own Model


class Post < ActiveRecord::Base
has_many :comments
def find_valid_comments
self.comment.find(:all, :conditions => { :is_spam => false },
:limit => 10)
end
end
class Comment < ActiveRecord::Base
belongs_to :post
end
class CommentsController < ApplicationController
def index
@comments = @post.find_valid_comments
end
end

After

1. Keep Finders on Their Own Model


class Post < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
named_scope :only_valid, :conditions => { :is_spam => false }
named_scope :limit, lambda { |size| { :limit => size } }
end
class CommentsController < ApplicationController
def index
@comments = @post.comments.only_valid.limit(10)
end
end

2. Love named_scope

Before

class PostController < ApplicationController


def search
conditions = { :title => "%#{params[:title]}%" } if params[:title]
conditions.merge!{ :content => "%#{params[:content]}%" } if params[:content]
case params[:order]
when "title" : order = "title desc"
when "created_at" : order = "created_at"
end
if params[:is_published]
conditions.merge!{ :is_published => true }
end
@posts = Post.find(:all, :conditions => conditions, :order => order,
:limit => params[:limit])
end
end
example code from Rails Antipatterns book

2. Love named_scope
class Post < ActiveRecord::Base
named_scope :matching, lambda { |column, value|
return {} if value.blank?
{ :conditions => ["#{column} like ?", "%#{value}%"] }
}
named_scope :order, lambda { |order|
{ :order => case order
when "title" : "title desc"
when "created_at" : "created_at"
end }
}
end

After

After

class PostController < ApplicationController


def search
@posts = Post.matching(:title, params[:title])
.matching(:content, params[:content])
.order(params[:order])
end
end

Before

3. the Law of Demeter


class Invoice < ActiveRecord::Base
belongs_to :user
end
<%= @invoice.user.name %>
<%= @invoice.user.address %>
<%= @invoice.user.cellphone %>

After

3. the Law of Demeter


class Invoice < ActiveRecord::Base
belongs_to :user
delegate :name, :address, :cellphone, :to => :user,
:prefix => true
end

<%= @invoice.user_name %>


<%= @invoice.user_address %>
<%= @invoice.user_cellphone %>

4. DRY: Metaprogramming
class Post < ActiveRecord::Base
validate_inclusion_of :status, :in => ['draft', 'published', 'spam']
def self.all_draft
find(:all, :conditions => { :status => 'draft' }
end
def self.all_published
find(:all, :conditions => { :status => 'published' }
end
def self.all_spam
find(:all, :conditions => { :status => 'spam' }
end
def draft?
self.stats == 'draft'
end
def published?
self.stats == 'published'
end
def spam?
self.stats == 'spam'
end
end

Before

4. DRY: Metaprogramming
class Post < ActiveRecord::Base
STATUSES = ['draft', 'published', 'spam']
validate_inclusion_of :status, :in => STATUSES
class << self
STATUSES.each do |status_name|
define_method "all_#{status}" do
find(:all, :conditions => { :status => status_name }
end
end
end
STATUSES.each do |status_name|
define_method "#{status_name}?" do
self.status == status_name
end
end
end

After

Breaking Up Models
Model

Before

5. Extract into Module


class User < ActiveRecord::Base
validates_presence_of :cellphone
before_save :parse_cellphone
def parse_cellphone
# do something
end
end

After

# /lib/has_cellphone.rb
module HasCellphone
def self.included(base)
base.validates_presence_of :cellphone
base.before_save :parse_cellphone
base.send(:include,InstanceMethods)
base.send(:extend, ClassMethods)
end
module InstanceMethods
def parse_cellphone
# do something
end
end
module ClassMethods
end
end

After

class User < ActiveRecord::Base


include HasCellphone
end

6. Extract to composed class


# == Schema Information
# address_city
:string(255)
# address_street
:string(255)
class Customer < ActiveRecord::Base
def adddress_close_to?(other_customer)
address_city == other_customer.address_city
end
def address_equal(other_customer)
address_street == other_customer.address_street &&
address_city == other_customer.address_city
end
end

Before

6. Extract to composed class

After

(value object)
class Customer < ActiveRecord::Base
composed_of :address, :mapping => [ %w(address_street street),
%w(address_city city) ]
end
class Address
attr_reader :street, :city
def initialize(street, city)
@street, @city = street, city
end
def close_to?(other_address)
city == other_address.city
end
def ==(other_address)
city == other_address.city && street == other_address.street
end
end
example code from Agile Web Development with Rails 3rd.

7. Use Observer
class Project < ActiveRecord::Base
after_create :send_create_notifications
private
def send_create_notifications
self.members.each do |member|
ProjectNotifier.deliver_notification(self, member)
end
end
end

Before

7. Use Observer
class Project < ActiveRecord::Base
# nothing here
end
# app/observers/project_notification_observer.rb
class ProjectNotificationObserver < ActiveRecord::Observer
observe Project
def after_create(project)
project.members.each do |member|
ProjectMailer.deliver_notice(project, member)
end
end
end

After

Best Practice Lesson 4:

Migration

1. Isolating Seed Data


class CreateRoles < ActiveRecord::Migration
def self.up
create_table "roles", :force => true do |t|
t.string :name
end
["admin", "author", "editor","account"].each do |name|
Role.create!(:name => name)
end
end
def self.down
drop_table "roles"
end
end

Before

1. Isolating Seed Data


# /db/seeds.rb (Rails 2.3.4)
["admin", "author", "editor","account"].each do |name|
Role.create!(:name => name)
end
rake db:seed

After

After

# /lib/tasks/dev.rake (before Rails 2.3.4)


namespace :dev do
desc "Setup seed data"
task :setup => :environment do
["admin", "author", "editor","account"].each do |name|
Role.create!(:name => name)
end
end
end
rake dev:setup

2. Always add DB index


class CreateComments < ActiveRecord::Migration
def self.up
create_table "comments", :force => true do |t|
t.string :content
t.integer :post_id
t.integer :user_id
end
end
def self.down
drop_table "comments"
end
end

Before

2. Always add DB index


class CreateComments < ActiveRecord::Migration
def self.up
create_table "comments", :force => true do |t|
t.string :content
t.integer :post_id
t.integer :user_id
end
add_index :comments, :post_id
add_index :comments, :user_id
end
def self.down
drop_table "comments"
end
end

After

Best Practice Lesson 5:

Controller

1. Use before_filter
class PostController < ApplicationController
def show
@post = current_user.posts.find(params[:id]
end
def edit
@post = current_user.posts.find(params[:id]
end
def update
@post = current_user.posts.find(params[:id]
@post.update_attributes(params[:post])
end
def destroy
@post = current_user.posts.find(params[:id]
@post.destroy
end
end

Before

1. Use before_filter

After

class PostController < ApplicationController


before_filter :find_post, :only => [:show, :edit, :update, :destroy]
def update
@post.update_attributes(params[:post])
end
def destroy
@post.destroy
end
protected
def find_post
@post = current_user.posts.find(params[:id])
end
end

2. DRY Controller

Before

class PostController < ApplicationController


def index
@posts = Post.all
end

def edit
@post = Post.find(params[:id)
end

def show
@post = Post.find(params[:id)
end

def update
@post = Post.find(params[:id)
@post.update_attributes(params[:post])
redirect_to post_path(@post)
end

def new
@post = Post.new
end
def create
@post.create(params[:post]
redirect_to post_path(@post)
end
end

def destroy
@post = Post.find(params[:id)
@post.destroy
redirect_to posts_path
end

2. DRY Controller
http://github.com/josevalim/inherited_resources
class PostController < InheritedResources::Base
# magic!! nothing here!
end

After

2. DRY Controller
class PostController < InheritedResources::Base
# if you need customize redirect url
def create
create! do |success, failure|
seccess.html { redirect_to post_url(@post) }
failure.html { redirect_to root_url }
end
end
end

After

DRY Controller Debate!!

You lose intent and readability


Deviating from standards makes it harder
to work with other programmers

Upgrading rails

from http://www.binarylogic.com/2009/10/06/discontinuing-resourcelogic/

Best Practice Lesson 6:

View

Never logic code in Views

1. Move code into controller


Before

After

<% @posts = Post.find(:all) %>


<% @posts.each do |post| %>
<%=h post.title %>
<%=h post.content %>
<% end %>

class PostsController < ApplicationController


def index
@posts = Post.find(:all)
end
end

2. Move code into model


Before

After

<% if current_user && (current_user == @post.user ||


@post.editors.include?(current_user) %>
<%= link_to 'Edit this post', edit_post_url(@post) %>
<% end %>

<% if @post.editable_by?(current_user) %>


<%= link_to 'Edit this post', edit_post_url(@post) %>
<% end %>

class Post < ActiveRecord::Base


def ediable_by?(user)
user && ( user == self.user || self.editors.include?(user)
end
end

Before

3. Move code into helper

<%= select_tag :state, options_for_select( [[t(:draft),"draft" ],


[t(:published),"published"]],
params[:default_state] ) %>

After

<%= select_tag :state, options_for_post_state(params[:default_state]) %>


# /app/helpers/posts_helper.rb
def options_for_post_state(default_state)
options_for_select( [[t(:draft),"draft" ],[t(:published),"published"]],
default_state )
end

4. Replace instance variable


with local variable
class Post < ApplicationController
def show
@post = Post.find(params[:id)
end
end

Before

<%= render :partial => "sidebar" %>

After

<%= render :partial => "sidebar", :locals => { :post => @post } %>

5. Use Form Builder


<% form_for @post do |f| %>
<p>
<%= f.label :title, t("post.title") %> <br>
<%= f.text_field :title %>
</p>
<p>
<%= f.label :content %> <br>
<%= f.text_area :content, :size => '80x20' %>
</p>
<p>
<%= f.submit t("submit") %>
</p>
<% end %>

Before

5. Use Form Builder


<% my_form_for @post do |f| %>
<%= f.text_field :title, :label => t("post.title") %>
<%= f.text_area :content, :size => '80x20',
:label => t("post.content") %>
<%= f.submit t("submit") %>
<% end %>

After

After

module ApplicationHelper
def my_form_for(*args, &block)
options = args.extract_options!.merge(:builder =>
LabeledFormBuilder)
form_for(*(args + [options]), &block)
end
end
class MyFormBuilder < ActionView::Helpers::FormBuilder
%w[text_field text_area].each do |method_name|
define_method(method_name) do |field_name, *args|
@template.content_tag(:p, field_label(field_name, *args) +
"<br />" + field_error(field_name) + super)
end
end
def submit(*args)
@template.content_tag(:p, super)
end
end

6. Organize Helper files


Before

After

#
#
#
#

app/helpers/user_posts_helper.rb
app/helpers/author_posts_helper.rb
app/helpers/editor_posts_helper.rb
app/helpers/admin_posts_helper.rb

class ApplicationController < ActionController::Base


helper :all # include all helpers, all the time
end
# app/helpers/posts_helper.rb

7. Learn Rails Helpers

Learn content_for and yield


Learn how to pass block parameter in helper

my slide about helper: http://www.slideshare.net/ihower/building-web-interface-on-rails

Read Rails helpers source code


/actionpack-x.y.z/action_view/helpers/*

Best Practice Lesson 7:

Code Refactoring

We have Ruby edition now!!


Must read it!

Reference:
:
http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model
http://www.matthewpaulmoore.com/ruby-on-rails-code-quality-checklist
http://www.chadfowler.com/2009/4/1/20-rails-development-no-no-s
:
Pragmatic Patterns of Ruby on Rails
Advanced Active Record Techniques Best Practice Refactoring Chad Pytel
Refactoring Your Rails Application RailsConf 2008
The Worst Rails Code You've Ever Seen Obie Fernandez
Mastering Rails Forms screencasts with Ryan Bates
:
Agile Software Development: Principles, Patterns, and Practices
AWDwR 3rd
The Rails Way 2nd.
Advanced Rails Recipes
Refactoring Ruby Edition
Ruby Best Practices
Enterprise Rails
Rails Antipatterns
Rails Rescue Handbook
Code Review (PeepCode)
Plugin Patterns (PeepCode)

More best practices:

Rails Performance
Rails Security

http://www.slideshare.net/ihower/rails-performance

http://www.slideshare.net/ihower/rails-security-3299368


Thank you.

You might also like