SlideShare a Scribd company logo
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: What’s 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):難以修改,每改一處牽一髮動全身
• 脆弱 (Fragility):一旦修改,別的無關地方也炸到
• 固定 (Immobility):難以分解,讓程式再重用
• 黏滯 (Viscosity):彈性不夠,把事情做對比做錯還難
• 不需要的複雜度 (Needless Complexity):過度設計沒
直接好處的基礎設施
• 不需要的重複 (Needless Repetition):相同概念的程
式碼被複製貼上重複使用
• 晦澀 (Opacity):難以閱讀,無法了解意圖
出自 Agile Software Development: Principles, Patterns, and Practices 一書
We need good code:
我們需要好程式
What’s 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
class PostsController < ApplicationController
def index
@public_posts = Post.find(:all, :conditions => { :state => 'public' },
:limit => 10,
:order => 'created_at desc')
@draft_posts = Post.find(:all, :conditions => { :state => 'draft' },
:limit => 10,
:order => 'created_at desc')
end
end
Before
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
1.Move finder to named_scope
After
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
class PostsController < ApplicationController
def create
@post = current_user.posts.build(params[:post])
@post.save
end
end
class User < ActiveRecord::Base
has_many :posts
end
2. Use model association
After
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
3. Use scope access
不必要的權限檢查
Before
class PostsController < ApplicationController
def edit
# raise RecordNotFound exception (404 error) if not found
@post = current_user.posts.find(params[:id)
end
end
After
3. Use scope access
找不到自然會丟例外
4.Add model virtual attribute
<% 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
Before
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
<% 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
After
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
<% 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
After
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
class InvoiceController < ApplicationController
def create
@invoice = Invoice.new_by_user(params[:invoice], current_user)
@invoice.save
end
end
After
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
class PostController < ApplicationController
def publish
@post = Post.find(params[:id])
@post.publish
redirect_to post_url(@post)
end
end
After
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
<% 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
Before
<% 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
After
Before
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 %>
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.
Before
After
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
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 Bates’s 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
class EventsController < ApplicationController
def index
end
def show
end
def create
end
def update
end
def destroy
end
end
def watch_list
end
def add_favorite
end
def invite
end
def join
end
def leave
end
def feeds
end
def add_comment
end
def show_comment
end
def destroy_comment
end
def edit_comment
end
def approve_comment
end
def white_member_list
end
def black_member_list
end
def deny_user
end
def allow_user
end
def edit_managers
end
def set_user_as_manager
end
def set_user_as_member
end
Before
After
class EventsController < ApplicationController
def index; end
def show; end
end
class CommentsControlers < ApplicationController
def index; end
def create; end
def destroy; end
end
def FavoriteControllers < ApplicationController
def create; end
def destroy; end
end
class EventMembershipsControlers < ApplicationController
def create; end
def destroy; end
end
1. Overuse route customizations
map.resources :posts, :member => { :comments => :get,
:create_comment => :post,
:update_comment => :post,
:delete_comment => :post }
Before
1. Overuse route customizations
Find another resources
map.resources :posts do |post|
post.resources :comments
end
After
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
過度設計: Never more than one level
Before
map.resources :posts do |post|
post.resources :comments do |comment|
comment.resources :favorites
end
end
<%= link_to post_comment_favorite_path(@post, @comment, @favorite) %>
After
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) %>
2. Needless deep nesting
過度設計: Never more than one level
3. Not use default route
Before
map.resources :posts, :member => { :push => :post }
map.connect ':controller/:action/:id'
map.connect ':controller/:action/:id.:format'
3. Not use default route
After
map.resources :posts, :member => { :push => :post }
#map.connect ':controller/:action/:id'
#map.connect ':controller/:action/:id.:format'
map.connect 'special/:action/:id', :controller => 'special'
Best Practice Lesson 3:
Model
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
Before
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
After
2. Love named_scope
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
Before
example code from Rails Antipatterns book
2. Love named_scope
After
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
class PostController < ApplicationController
def search
@posts = Post.matching(:title, params[:title])
.matching(:content, params[:content])
.order(params[:order])
end
end
3. the Law of Demeter
class Invoice < ActiveRecord::Base
belongs_to :user
end
<%= @invoice.user.name %>
<%= @invoice.user.address %>
<%= @invoice.user.cellphone %>
Before
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 %>
After
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 減重
5. Extract into Module
class User < ActiveRecord::Base
validates_presence_of :cellphone
before_save :parse_cellphone
def parse_cellphone
# do something
end
end
Before
# /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
After
6. Extract to composed class
Before
# == 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
After
6. Extract to composed class
(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
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
7. Use Observer
After
Best Practice Lesson 4:
Migration
1. Isolating Seed Data
Before
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
1. Isolating Seed Data
After
# /db/seeds.rb (Rails 2.3.4)
["admin", "author", "editor","account"].each do |name|
Role.create!(:name => name)
end
rake db:seed
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
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
After
2. DRY Controller
class PostController < ApplicationController
def index
@posts = Post.all
end
def show
@post = Post.find(params[:id)
end
def new
@post = Post.new
end
def create
@post.create(params[:post]
redirect_to post_path(@post)
end
end
Before
def edit
@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 destroy
@post = Post.find(params[:id)
@post.destroy
redirect_to posts_path
end
After
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
• You lose intent and readability
• Deviating from standards makes it harder
to work with other programmers
• Upgrading rails
DRY Controller Debate!!
小心走火入魔
from http://www.binarylogic.com/2009/10/06/discontinuing-resourcelogic/
Best Practice Lesson 6:
View
最重要的守則:
Never logic code inViews
1. Move code into controller
<% @posts = Post.find(:all) %>
<% @posts.each do |post| %>
<%=h post.title %>
<%=h post.content %>
<% end %>
Before
class PostsController < ApplicationController
def index
@posts = Post.find(:all)
end
end
After
2. Move code into model
<% 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
After
3. Move code into helper
<%= select_tag :state, options_for_select( [[t(:draft),"draft" ],
[t(:published),"published"]],
params[:default_state] ) %>
Before
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
<%= render :partial => "sidebar" %>
<%= render :partial => "sidebar", :locals => { :post => @post } %>
Before
After
class Post < ApplicationController
def show
@post = Post.find(params[:id)
end
end
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
After
<% 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 %>
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
After
6. Organize Helper files
# 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
Before
After
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
RefactoringYour Rails Application RailsConf 2008
The Worst Rails CodeYou'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
http://www.slideshare.net/ihower/rails-performance
• Rails Security
http://www.slideshare.net/ihower/rails-security-3299368
感謝聆聽,請多指教。
Thank you.

More Related Content

Viewers also liked

10 Ruby and Rails Pro Tips
10 Ruby and Rails Pro Tips10 Ruby and Rails Pro Tips
10 Ruby and Rails Pro Tips
Michel Pigassou
 
How To Create Presentation Slides That Are Out Of This World by @slidecomet @...
How To Create Presentation Slides That Are Out Of This World by @slidecomet @...How To Create Presentation Slides That Are Out Of This World by @slidecomet @...
How To Create Presentation Slides That Are Out Of This World by @slidecomet @...
HighSpark | Visual Storytelling Agency
 
Ruby on rails. Best practices
Ruby on rails. Best practicesRuby on rails. Best practices
Ruby on rails. Best practices
Vladimir Tkach
 
Rails Best Practices
Rails Best PracticesRails Best Practices
Rails Best Practices
Icalia Labs
 
MongoDB San Francisco 2013: Schema design presented by Jason Zucchetto, Consu...
MongoDB San Francisco 2013: Schema design presented by Jason Zucchetto, Consu...MongoDB San Francisco 2013: Schema design presented by Jason Zucchetto, Consu...
MongoDB San Francisco 2013: Schema design presented by Jason Zucchetto, Consu...
MongoDB
 
Boost your productivity!: Productivity tips for rails developers - Lightning ...
Boost your productivity!: Productivity tips for rails developers - Lightning ...Boost your productivity!: Productivity tips for rails developers - Lightning ...
Boost your productivity!: Productivity tips for rails developers - Lightning ...
Alberto Perdomo
 
Ruby 1.9
Ruby 1.9Ruby 1.9
Ruby 1.9
Wen-Tien Chang
 
Ruby on Rails Vs. ASP.NET MVC
Ruby on Rails Vs. ASP.NET MVCRuby on Rails Vs. ASP.NET MVC
Ruby on Rails Vs. ASP.NET MVC
Shay Friedman
 
Workshop HTML5+PhoneGap by Ivano Malavolta
Workshop HTML5+PhoneGap by Ivano Malavolta Workshop HTML5+PhoneGap by Ivano Malavolta
Workshop HTML5+PhoneGap by Ivano Malavolta
Commit University
 
Introduction to Active Record - Silicon Valley Ruby Conference 2007
Introduction to Active Record - Silicon Valley Ruby Conference 2007Introduction to Active Record - Silicon Valley Ruby Conference 2007
Introduction to Active Record - Silicon Valley Ruby Conference 2007
Rabble .
 
Large data with Scikit-learn - Boston Data Mining Meetup - Alex Perrier
Large data with Scikit-learn - Boston Data Mining Meetup  - Alex PerrierLarge data with Scikit-learn - Boston Data Mining Meetup  - Alex Perrier
Large data with Scikit-learn - Boston Data Mining Meetup - Alex Perrier
Alexis Perrier
 
Tips for creating a Self Organizing Teams
Tips for creating a Self Organizing TeamsTips for creating a Self Organizing Teams
Tips for creating a Self Organizing Teams
Yves Hanoulle
 
Overhead crane modernizations
Overhead crane modernizationsOverhead crane modernizations
Overhead crane modernizations
Konecranes
 
Learn Dashing Widget in 90 minutes
Learn Dashing Widget in 90 minutesLearn Dashing Widget in 90 minutes
Learn Dashing Widget in 90 minutes
Larry Cai
 
WSO2 para o Governo Brasileiro
WSO2 para o  Governo BrasileiroWSO2 para o  Governo Brasileiro
WSO2 para o Governo Brasileiro
Edgar Silva
 
Adaptive pre-processing for streaming data
Adaptive pre-processing for streaming dataAdaptive pre-processing for streaming data
Adaptive pre-processing for streaming data
LARCA UPC
 
Comparing JVM Web Frameworks - Devoxx France 2013
Comparing JVM Web Frameworks - Devoxx France 2013Comparing JVM Web Frameworks - Devoxx France 2013
Comparing JVM Web Frameworks - Devoxx France 2013
Matt Raible
 
Topic modeling of Twitter followers - Paris Machine Learning meetup - Alex Pe...
Topic modeling of Twitter followers - Paris Machine Learning meetup - Alex Pe...Topic modeling of Twitter followers - Paris Machine Learning meetup - Alex Pe...
Topic modeling of Twitter followers - Paris Machine Learning meetup - Alex Pe...
Alexis Perrier
 
RequireJS & Handlebars
RequireJS & HandlebarsRequireJS & Handlebars
RequireJS & Handlebars
Ivano Malavolta
 
Overhead Crane Manual
Overhead Crane ManualOverhead Crane Manual
Overhead Crane Manual
scojetinc
 

Viewers also liked (20)

10 Ruby and Rails Pro Tips
10 Ruby and Rails Pro Tips10 Ruby and Rails Pro Tips
10 Ruby and Rails Pro Tips
 
How To Create Presentation Slides That Are Out Of This World by @slidecomet @...
How To Create Presentation Slides That Are Out Of This World by @slidecomet @...How To Create Presentation Slides That Are Out Of This World by @slidecomet @...
How To Create Presentation Slides That Are Out Of This World by @slidecomet @...
 
Ruby on rails. Best practices
Ruby on rails. Best practicesRuby on rails. Best practices
Ruby on rails. Best practices
 
Rails Best Practices
Rails Best PracticesRails Best Practices
Rails Best Practices
 
MongoDB San Francisco 2013: Schema design presented by Jason Zucchetto, Consu...
MongoDB San Francisco 2013: Schema design presented by Jason Zucchetto, Consu...MongoDB San Francisco 2013: Schema design presented by Jason Zucchetto, Consu...
MongoDB San Francisco 2013: Schema design presented by Jason Zucchetto, Consu...
 
Boost your productivity!: Productivity tips for rails developers - Lightning ...
Boost your productivity!: Productivity tips for rails developers - Lightning ...Boost your productivity!: Productivity tips for rails developers - Lightning ...
Boost your productivity!: Productivity tips for rails developers - Lightning ...
 
Ruby 1.9
Ruby 1.9Ruby 1.9
Ruby 1.9
 
Ruby on Rails Vs. ASP.NET MVC
Ruby on Rails Vs. ASP.NET MVCRuby on Rails Vs. ASP.NET MVC
Ruby on Rails Vs. ASP.NET MVC
 
Workshop HTML5+PhoneGap by Ivano Malavolta
Workshop HTML5+PhoneGap by Ivano Malavolta Workshop HTML5+PhoneGap by Ivano Malavolta
Workshop HTML5+PhoneGap by Ivano Malavolta
 
Introduction to Active Record - Silicon Valley Ruby Conference 2007
Introduction to Active Record - Silicon Valley Ruby Conference 2007Introduction to Active Record - Silicon Valley Ruby Conference 2007
Introduction to Active Record - Silicon Valley Ruby Conference 2007
 
Large data with Scikit-learn - Boston Data Mining Meetup - Alex Perrier
Large data with Scikit-learn - Boston Data Mining Meetup  - Alex PerrierLarge data with Scikit-learn - Boston Data Mining Meetup  - Alex Perrier
Large data with Scikit-learn - Boston Data Mining Meetup - Alex Perrier
 
Tips for creating a Self Organizing Teams
Tips for creating a Self Organizing TeamsTips for creating a Self Organizing Teams
Tips for creating a Self Organizing Teams
 
Overhead crane modernizations
Overhead crane modernizationsOverhead crane modernizations
Overhead crane modernizations
 
Learn Dashing Widget in 90 minutes
Learn Dashing Widget in 90 minutesLearn Dashing Widget in 90 minutes
Learn Dashing Widget in 90 minutes
 
WSO2 para o Governo Brasileiro
WSO2 para o  Governo BrasileiroWSO2 para o  Governo Brasileiro
WSO2 para o Governo Brasileiro
 
Adaptive pre-processing for streaming data
Adaptive pre-processing for streaming dataAdaptive pre-processing for streaming data
Adaptive pre-processing for streaming data
 
Comparing JVM Web Frameworks - Devoxx France 2013
Comparing JVM Web Frameworks - Devoxx France 2013Comparing JVM Web Frameworks - Devoxx France 2013
Comparing JVM Web Frameworks - Devoxx France 2013
 
Topic modeling of Twitter followers - Paris Machine Learning meetup - Alex Pe...
Topic modeling of Twitter followers - Paris Machine Learning meetup - Alex Pe...Topic modeling of Twitter followers - Paris Machine Learning meetup - Alex Pe...
Topic modeling of Twitter followers - Paris Machine Learning meetup - Alex Pe...
 
RequireJS & Handlebars
RequireJS & HandlebarsRequireJS & Handlebars
RequireJS & Handlebars
 
Overhead Crane Manual
Overhead Crane ManualOverhead Crane Manual
Overhead Crane Manual
 

More from Wen-Tien Chang

⼤語⾔模型 LLM 應⽤開發入⾨
⼤語⾔模型 LLM 應⽤開發入⾨⼤語⾔模型 LLM 應⽤開發入⾨
⼤語⾔模型 LLM 應⽤開發入⾨
Wen-Tien Chang
 
Ruby Rails 老司機帶飛
Ruby Rails 老司機帶飛Ruby Rails 老司機帶飛
Ruby Rails 老司機帶飛
Wen-Tien Chang
 
A brief introduction to Machine Learning
A brief introduction to Machine LearningA brief introduction to Machine Learning
A brief introduction to Machine Learning
Wen-Tien Chang
 
淺談 Startup 公司的軟體開發流程 v2
淺談 Startup 公司的軟體開發流程 v2淺談 Startup 公司的軟體開發流程 v2
淺談 Startup 公司的軟體開發流程 v2
Wen-Tien Chang
 
RSpec on Rails Tutorial
RSpec on Rails TutorialRSpec on Rails Tutorial
RSpec on Rails Tutorial
Wen-Tien Chang
 
RSpec & TDD Tutorial
RSpec & TDD TutorialRSpec & TDD Tutorial
RSpec & TDD Tutorial
Wen-Tien Chang
 
ALPHAhackathon: How to collaborate
ALPHAhackathon: How to collaborateALPHAhackathon: How to collaborate
ALPHAhackathon: How to collaborate
Wen-Tien Chang
 
Git 版本控制系統 -- 從微觀到宏觀
Git 版本控制系統 -- 從微觀到宏觀Git 版本控制系統 -- 從微觀到宏觀
Git 版本控制系統 -- 從微觀到宏觀Wen-Tien Chang
 
Exception Handling: Designing Robust Software in Ruby (with presentation note)
Exception Handling: Designing Robust Software in Ruby (with presentation note)Exception Handling: Designing Robust Software in Ruby (with presentation note)
Exception Handling: Designing Robust Software in Ruby (with presentation note)
Wen-Tien Chang
 
Exception Handling: Designing Robust Software in Ruby
Exception Handling: Designing Robust Software in RubyException Handling: Designing Robust Software in Ruby
Exception Handling: Designing Robust Software in Ruby
Wen-Tien Chang
 
從 Classes 到 Objects: 那些 OOP 教我的事
從 Classes 到 Objects: 那些 OOP 教我的事從 Classes 到 Objects: 那些 OOP 教我的事
從 Classes 到 Objects: 那些 OOP 教我的事Wen-Tien Chang
 
Yet another introduction to Git - from the bottom up
Yet another introduction to Git - from the bottom upYet another introduction to Git - from the bottom up
Yet another introduction to Git - from the bottom upWen-Tien Chang
 
A brief introduction to Vagrant – 原來 VirtualBox 可以這樣玩
A brief introduction to Vagrant – 原來 VirtualBox 可以這樣玩A brief introduction to Vagrant – 原來 VirtualBox 可以這樣玩
A brief introduction to Vagrant – 原來 VirtualBox 可以這樣玩Wen-Tien Chang
 
Ruby 程式語言綜覽簡介
Ruby 程式語言綜覽簡介Ruby 程式語言綜覽簡介
Ruby 程式語言綜覽簡介
Wen-Tien Chang
 
A brief introduction to SPDY - 邁向 HTTP/2.0
A brief introduction to SPDY - 邁向 HTTP/2.0A brief introduction to SPDY - 邁向 HTTP/2.0
A brief introduction to SPDY - 邁向 HTTP/2.0
Wen-Tien Chang
 
RubyConf Taiwan 2012 Opening & Closing
RubyConf Taiwan 2012 Opening & ClosingRubyConf Taiwan 2012 Opening & Closing
RubyConf Taiwan 2012 Opening & Closing
Wen-Tien Chang
 
從 Scrum 到 Kanban: 為什麼 Scrum 不適合 Lean Startup
從 Scrum 到 Kanban: 為什麼 Scrum 不適合 Lean Startup從 Scrum 到 Kanban: 為什麼 Scrum 不適合 Lean Startup
從 Scrum 到 Kanban: 為什麼 Scrum 不適合 Lean StartupWen-Tien Chang
 
那些 Functional Programming 教我的事
那些 Functional Programming 教我的事那些 Functional Programming 教我的事
那些 Functional Programming 教我的事
Wen-Tien Chang
 
RubyConf Taiwan 2011 Opening & Closing
RubyConf Taiwan 2011 Opening & ClosingRubyConf Taiwan 2011 Opening & Closing
RubyConf Taiwan 2011 Opening & ClosingWen-Tien Chang
 

More from Wen-Tien Chang (20)

⼤語⾔模型 LLM 應⽤開發入⾨
⼤語⾔模型 LLM 應⽤開發入⾨⼤語⾔模型 LLM 應⽤開發入⾨
⼤語⾔模型 LLM 應⽤開發入⾨
 
Ruby Rails 老司機帶飛
Ruby Rails 老司機帶飛Ruby Rails 老司機帶飛
Ruby Rails 老司機帶飛
 
A brief introduction to Machine Learning
A brief introduction to Machine LearningA brief introduction to Machine Learning
A brief introduction to Machine Learning
 
淺談 Startup 公司的軟體開發流程 v2
淺談 Startup 公司的軟體開發流程 v2淺談 Startup 公司的軟體開發流程 v2
淺談 Startup 公司的軟體開發流程 v2
 
RSpec on Rails Tutorial
RSpec on Rails TutorialRSpec on Rails Tutorial
RSpec on Rails Tutorial
 
RSpec & TDD Tutorial
RSpec & TDD TutorialRSpec & TDD Tutorial
RSpec & TDD Tutorial
 
ALPHAhackathon: How to collaborate
ALPHAhackathon: How to collaborateALPHAhackathon: How to collaborate
ALPHAhackathon: How to collaborate
 
Git 版本控制系統 -- 從微觀到宏觀
Git 版本控制系統 -- 從微觀到宏觀Git 版本控制系統 -- 從微觀到宏觀
Git 版本控制系統 -- 從微觀到宏觀
 
Exception Handling: Designing Robust Software in Ruby (with presentation note)
Exception Handling: Designing Robust Software in Ruby (with presentation note)Exception Handling: Designing Robust Software in Ruby (with presentation note)
Exception Handling: Designing Robust Software in Ruby (with presentation note)
 
Exception Handling: Designing Robust Software in Ruby
Exception Handling: Designing Robust Software in RubyException Handling: Designing Robust Software in Ruby
Exception Handling: Designing Robust Software in Ruby
 
從 Classes 到 Objects: 那些 OOP 教我的事
從 Classes 到 Objects: 那些 OOP 教我的事從 Classes 到 Objects: 那些 OOP 教我的事
從 Classes 到 Objects: 那些 OOP 教我的事
 
Yet another introduction to Git - from the bottom up
Yet another introduction to Git - from the bottom upYet another introduction to Git - from the bottom up
Yet another introduction to Git - from the bottom up
 
A brief introduction to Vagrant – 原來 VirtualBox 可以這樣玩
A brief introduction to Vagrant – 原來 VirtualBox 可以這樣玩A brief introduction to Vagrant – 原來 VirtualBox 可以這樣玩
A brief introduction to Vagrant – 原來 VirtualBox 可以這樣玩
 
Ruby 程式語言綜覽簡介
Ruby 程式語言綜覽簡介Ruby 程式語言綜覽簡介
Ruby 程式語言綜覽簡介
 
A brief introduction to SPDY - 邁向 HTTP/2.0
A brief introduction to SPDY - 邁向 HTTP/2.0A brief introduction to SPDY - 邁向 HTTP/2.0
A brief introduction to SPDY - 邁向 HTTP/2.0
 
RubyConf Taiwan 2012 Opening & Closing
RubyConf Taiwan 2012 Opening & ClosingRubyConf Taiwan 2012 Opening & Closing
RubyConf Taiwan 2012 Opening & Closing
 
從 Scrum 到 Kanban: 為什麼 Scrum 不適合 Lean Startup
從 Scrum 到 Kanban: 為什麼 Scrum 不適合 Lean Startup從 Scrum 到 Kanban: 為什麼 Scrum 不適合 Lean Startup
從 Scrum 到 Kanban: 為什麼 Scrum 不適合 Lean Startup
 
Git Tutorial 教學
Git Tutorial 教學Git Tutorial 教學
Git Tutorial 教學
 
那些 Functional Programming 教我的事
那些 Functional Programming 教我的事那些 Functional Programming 教我的事
那些 Functional Programming 教我的事
 
RubyConf Taiwan 2011 Opening & Closing
RubyConf Taiwan 2011 Opening & ClosingRubyConf Taiwan 2011 Opening & Closing
RubyConf Taiwan 2011 Opening & Closing
 

Rails Best Practices

  • 1. Rails Best Practices ihower@gmail.com 張文鈿 2009/10 As this slide writing, the current Rails version is 2.3.4
  • 2. Who am I ? • 張文鈿 a.k.a. ihower • http://ihower.tw • http://twitter.com/ihower • http://github.com/ihower • 來自台灣新竹 (Hsinchu,Taiwan)
  • 4. Agenda • Concept: What’s good code? • Move Code from Controller to Model • RESTful best practices • Model best practices • Controller best practices • View best practices
  • 5. Warning! you should have testing before modify! 本次演講雖沒有提及測試, 但在修改重構程式前, 應有好的測試, 以確保程式於修改後執行無誤。
  • 6. Best Practice Lesson 0: Concepts
  • 7. Why best practices? • Large & complicated application 日漸複雜的程式 • Team & different coding style 團隊開發
  • 8. Your code become... • 僵硬 (Rigidity):難以修改,每改一處牽一髮動全身 • 脆弱 (Fragility):一旦修改,別的無關地方也炸到 • 固定 (Immobility):難以分解,讓程式再重用 • 黏滯 (Viscosity):彈性不夠,把事情做對比做錯還難 • 不需要的複雜度 (Needless Complexity):過度設計沒 直接好處的基礎設施 • 不需要的重複 (Needless Repetition):相同概念的程 式碼被複製貼上重複使用 • 晦澀 (Opacity):難以閱讀,無法了解意圖 出自 Agile Software Development: Principles, Patterns, and Practices 一書
  • 9. We need good code: 我們需要好程式
  • 10. What’s Good code? • Readability 易讀,容易了解 • Flexibility 彈性,容易擴充 • Effective 效率,撰碼快速 • Maintainability 維護性,容易找到問題 • Consistency 一致性,循慣例無需死背 • Testability 可測性,元件獨立容易測試
  • 11. So,What we can do? 來開始學幾招吧
  • 12. Best Practice Lesson 1: Move code from Controller to Model action code 超過15行請注意 http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model
  • 13. 1.Move finder to named_scope class PostsController < ApplicationController def index @public_posts = Post.find(:all, :conditions => { :state => 'public' }, :limit => 10, :order => 'created_at desc') @draft_posts = Post.find(:all, :conditions => { :state => 'draft' }, :limit => 10, :order => 'created_at desc') end end Before
  • 14. 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 1.Move finder to named_scope After
  • 15. 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
  • 16. class PostsController < ApplicationController def create @post = current_user.posts.build(params[:post]) @post.save end end class User < ActiveRecord::Base has_many :posts end 2. Use model association After
  • 17. 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 3. Use scope access 不必要的權限檢查 Before
  • 18. class PostsController < ApplicationController def edit # raise RecordNotFound exception (404 error) if not found @post = current_user.posts.find(params[:id) end end After 3. Use scope access 找不到自然會丟例外
  • 19. 4.Add model virtual attribute <% 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 Before
  • 20. 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
  • 21. <% 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 After
  • 22. 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
  • 23. 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
  • 24. <% 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 After
  • 25. 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
  • 26. 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
  • 27. class InvoiceController < ApplicationController def create @invoice = Invoice.new_by_user(params[:invoice], current_user) @invoice.save end end After
  • 28. 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
  • 29. 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
  • 30. class PostController < ApplicationController def publish @post = Post.find(params[:id]) @post.publish redirect_to post_url(@post) end end After
  • 31. 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
  • 32. <% 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 Before
  • 33. <% 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 After
  • 34. Before 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 %>
  • 35. 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. Before
  • 36. After 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
  • 37. After class Product < ApplicationController def create @product = Product.new(params[:product]) @product.save end end
  • 38. 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 %>
  • 39. Nested Model Forms before Rails 2.3 ? • Ryan Bates’s series of railscasts on complex forms • http://railscasts.com/episodes/75-complex-forms-part-3 • Recipe 13 in Advanced Rails Recipes book
  • 40. Best Practice Lesson 2: RESTful 請愛用 RESTful conventions
  • 41. Why RESTful? RESTful help you to organize/name controllers, routes and actions in standardization way
  • 42. class EventsController < ApplicationController def index end def show end def create end def update end def destroy end end def watch_list end def add_favorite end def invite end def join end def leave end def feeds end def add_comment end def show_comment end def destroy_comment end def edit_comment end def approve_comment end def white_member_list end def black_member_list end def deny_user end def allow_user end def edit_managers end def set_user_as_manager end def set_user_as_member end Before
  • 43. After class EventsController < ApplicationController def index; end def show; end end class CommentsControlers < ApplicationController def index; end def create; end def destroy; end end def FavoriteControllers < ApplicationController def create; end def destroy; end end class EventMembershipsControlers < ApplicationController def create; end def destroy; end end
  • 44. 1. Overuse route customizations map.resources :posts, :member => { :comments => :get, :create_comment => :post, :update_comment => :post, :delete_comment => :post } Before
  • 45. 1. Overuse route customizations Find another resources map.resources :posts do |post| post.resources :comments end After
  • 46. 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
  • 47. 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
  • 48. Learn RESTful design my slide about restful: http://www.slideshare.net/ihower/practical-rails2-350619
  • 49. 2. Needless deep nesting 過度設計: Never more than one level Before map.resources :posts do |post| post.resources :comments do |comment| comment.resources :favorites end end <%= link_to post_comment_favorite_path(@post, @comment, @favorite) %>
  • 50. After 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) %> 2. Needless deep nesting 過度設計: Never more than one level
  • 51. 3. Not use default route Before map.resources :posts, :member => { :push => :post } map.connect ':controller/:action/:id' map.connect ':controller/:action/:id.:format'
  • 52. 3. Not use default route After map.resources :posts, :member => { :push => :post } #map.connect ':controller/:action/:id' #map.connect ':controller/:action/:id.:format' map.connect 'special/:action/:id', :controller => 'special'
  • 54. 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 Before
  • 55. 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 After
  • 56. 2. Love named_scope 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 Before example code from Rails Antipatterns book
  • 57. 2. Love named_scope After 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
  • 58. After class PostController < ApplicationController def search @posts = Post.matching(:title, params[:title]) .matching(:content, params[:content]) .order(params[:order]) end end
  • 59. 3. the Law of Demeter class Invoice < ActiveRecord::Base belongs_to :user end <%= @invoice.user.name %> <%= @invoice.user.address %> <%= @invoice.user.cellphone %> Before
  • 60. 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 %> After
  • 61. 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
  • 62. 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
  • 63. Breaking Up Models 幫 Model 減重
  • 64. 5. Extract into Module class User < ActiveRecord::Base validates_presence_of :cellphone before_save :parse_cellphone def parse_cellphone # do something end end Before
  • 65. # /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
  • 66. class User < ActiveRecord::Base include HasCellphone end After
  • 67. 6. Extract to composed class Before # == 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
  • 68. After 6. Extract to composed class (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.
  • 69. 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
  • 70. 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 7. Use Observer After
  • 71. Best Practice Lesson 4: Migration
  • 72. 1. Isolating Seed Data Before 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
  • 73. 1. Isolating Seed Data After # /db/seeds.rb (Rails 2.3.4) ["admin", "author", "editor","account"].each do |name| Role.create!(:name => name) end rake db:seed
  • 74. 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
  • 75. 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
  • 76. 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
  • 77. Best Practice Lesson 5: Controller
  • 78. 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
  • 79. 1. Use before_filter 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 After
  • 80. 2. DRY Controller class PostController < ApplicationController def index @posts = Post.all end def show @post = Post.find(params[:id) end def new @post = Post.new end def create @post.create(params[:post] redirect_to post_path(@post) end end Before def edit @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 destroy @post = Post.find(params[:id) @post.destroy redirect_to posts_path end
  • 81. After 2. DRY Controller http://github.com/josevalim/inherited_resources class PostController < InheritedResources::Base # magic!! nothing here! end
  • 82. 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
  • 83. • You lose intent and readability • Deviating from standards makes it harder to work with other programmers • Upgrading rails DRY Controller Debate!! 小心走火入魔 from http://www.binarylogic.com/2009/10/06/discontinuing-resourcelogic/
  • 86. 1. Move code into controller <% @posts = Post.find(:all) %> <% @posts.each do |post| %> <%=h post.title %> <%=h post.content %> <% end %> Before class PostsController < ApplicationController def index @posts = Post.find(:all) end end After
  • 87. 2. Move code into model <% 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 After
  • 88. 3. Move code into helper <%= select_tag :state, options_for_select( [[t(:draft),"draft" ], [t(:published),"published"]], params[:default_state] ) %> Before 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
  • 89. 4. Replace instance variable with local variable <%= render :partial => "sidebar" %> <%= render :partial => "sidebar", :locals => { :post => @post } %> Before After class Post < ApplicationController def show @post = Post.find(params[:id) end end
  • 90. 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
  • 91. 5. Use Form Builder After <% 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 %>
  • 92. 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 After
  • 93. 6. Organize Helper files # 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 Before After
  • 94. 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/*
  • 95. Best Practice Lesson 7: Code Refactoring
  • 96. We have Ruby edition now!! Must read it!
  • 97. 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 RefactoringYour Rails Application RailsConf 2008 The Worst Rails CodeYou'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)
  • 98. More best practices: • Rails Performance http://www.slideshare.net/ihower/rails-performance • Rails Security http://www.slideshare.net/ihower/rails-security-3299368