Rails Best Practices: As This Slide Writing, The Current Rails Version Is 2.3.4
Rails Best Practices: As This Slide Writing, The Current Rails Version Is 2.3.4
[email protected]
張文鈿
2009/10
Concepts
Why best practices?
def index
@public_posts = Post.find(:all, :conditions => { :state => 'public' },
:limit => 10,
:order => 'created_at desc')
end
After
def index
@published_post = Post.published
@draft_post = Post.draft
end
end
end
Before
def create
@post = Post.new(params[:post])
@post.user_id = current_user.id
@post.save
end
end
After
def create
@post = current_user.posts.build(params[:post])
@post.save
end
end
不必要的權限檢查
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
After
找不到自然會丟例外
def edit
# raise RecordNotFound exception (404 error) if not found
@post = current_user.posts.find(params[:id)
end
end
Before
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
After
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
def create
@user = User.create(params[:user)
end
end
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
After
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
def create
@post = Post.new(params[:post])
@post.save
end
end
6. Replace Complex Creation Before
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
6. Replace Complex Creation
After
if Time.now.day > 15
invoice.delivery_time = Time.now + 2.month
else
invoice.delivery_time = Time.now + 1.month
end
end
end
After
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
7. Move Model Logic into the After
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
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
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
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) Before
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
<% end
After
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
RESTful
請愛用 RESTful conventions
Why RESTful?
RESTful help you to organize/name controllers, routes
and actions in standardization way
Before
def set_user_as_member
end
end
After
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
Before
map.connect ':controller/:action/:id'
map.connect ':controller/:action/:id.:format'
After
#map.connect ':controller/:action/:id'
#map.connect ':controller/:action/:id.:format'
Model
Before
def find_valid_comments
self.comment.find(:all, :conditions => { :is_spam => false },
:limit => 10)
end
end
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
end
2. Love named_scope
class Post < ActiveRecord::Base
end
After
def search
@posts = Post.matching(:title, params[:title])
.matching(:content, params[:content])
.order(params[:order])
end
end
Before
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
4. DRY: Metaprogramming
After
STATUSES.each do |status_name|
define_method "#{status_name}?" do
self.status == status_name
end
end
end
Breaking Up Models
幫 Model 減重
Before
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
include HasCellphone
end
Before
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
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.
Before
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
After
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
Best Practice Lesson 4:
Migration
Before
def self.down
drop_table "roles"
end
end
After
rake db:seed
After
namespace :dev do
end
rake dev:setup
Before
def self.down
drop_table "comments"
end
end
After
def self.down
drop_table "comments"
end
end
Best Practice Lesson 5:
Controller
1. Use before_filter Before
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
1. Use before_filter After
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
Before
2. DRY Controller
class PostController < ApplicationController
end
After
2. DRY Controller
http://github.com/josevalim/inherited_resources
end
After
2. DRY Controller
class PostController < InheritedResources::Base
end
DRY Controller Debate!!
小心走火入魔
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 <% @posts = Post.find(:all) %>
<% @posts.each do |post| %>
<%=h post.title %>
<%=h post.content %>
<% end %>
After
class PostsController < ApplicationController
def index
@posts = Post.find(:all)
end
end
2. Move code into model
Before <% if current_user && (current_user == @post.user ||
@post.editors.include?(current_user) %>
<%= link_to 'Edit this post', edit_post_url(@post) %>
<% end %>
After
# /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
After <%= render :partial => "sidebar", :locals => { :post => @post } %>
Before
<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>
def submit(*args)
@template.content_tag(:p, super)
end
end
6. Organize Helper files
# app/helpers/user_posts_helper.rb
Before # app/helpers/author_posts_helper.rb
# app/helpers/editor_posts_helper.rb
# app/helpers/admin_posts_helper.rb
# app/helpers/posts_helper.rb
7. Learn Rails Helpers
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
http://www.slideshare.net/ihower/rails-performance
• Rails Security
http://www.slideshare.net/ihower/rails-security-3299368
感謝聆聽,請多指教。
Thank you.