Rails Best Practices

114,860 views
112,710 views

Published on

see http://ihower.tw/blog/archives/3075 , this's my talk at http://kungfurails.com and http://ruby.tw

Published in: Automotive
21 Comments
349 Likes
Statistics
Notes
No Downloads
Views
Total views
114,860
On SlideShare
0
From Embeds
0
Number of Embeds
12,453
Actions
Shares
0
Downloads
2,877
Comments
21
Likes
349
Embeds 0
No embeds

No notes for slide

Rails Best Practices

  1. Rails Best Practices ihower@gmail.com 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) • ( ) • http://handlino.com • http://registrano.com
  3. Ruby Taiwan http://ruby.tw
  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. Before 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
  14. After 1.Move finder to named_scope 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
  15. Before 2. Use model association class PostsController < ApplicationController def create @post = Post.new(params[:post]) @post.user_id = current_user.id @post.save end end
  16. After 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
  17. 3. Use scope access Before 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
  18. 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
  19. Before 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
  20. After 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
  21. 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
  22. 5. Use model callback Before <% 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
  23. After 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
  24. 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
  25. 6. Replace Complex Creation Before 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
  26. 6. Replace Complex Creation After 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
  27. After class InvoiceController < ApplicationController def create @invoice = Invoice.new_by_user(params[:invoice], current_user) @invoice.save end end
  28. 7. Move Model Logic into the Before 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
  29. 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
  30. After class PostController < ApplicationController def publish @post = Post.find(params[:id]) @post.publish redirect_to post_url(@post) end end
  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. 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
  33. 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
  34. 9. Nested Model Forms (one-to-one) Before 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. 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.
  36. 9. Nested Model Forms (one-to-one) After 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. Before class EventsController < ApplicationController def index def white_member_list def watch_list def feeds end end end end def show def black_member_list def add_favorite def add_comment end end end end def create def deny_user def invite def show_comment end end end end def update def allow_user def join def destroy_comment end end end end def destroy def edit_managers def leave def edit_comment end end end end def approve_comment def set_user_as_manager end end def set_user_as_member end end
  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. Before 1. Overuse route customizations map.resources :posts, :member => { :comments => :get, :create_comment => :post, :update_comment => :post, :delete_comment => :post }
  45. After 1. Overuse route customizations Find another resources map.resources :posts do |post| post.resources :comments end
  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. Before 2. Needless deep nesting : 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) %>
  50. After 2. Needless deep nesting : 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) %>
  51. Before 3. Not use default route map.resources :posts, :member => { :push => :post } map.connect ':controller/:action/:id' map.connect ':controller/:action/:id.:format'
  52. After 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'
  53. Best Practice Lesson 3: Model
  54. 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
  55. 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
  56. Before 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 example code from Rails Antipatterns book
  57. After 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
  58. After class PostController < ApplicationController def search @posts = Post.matching(:title, params[:title]) .matching(:content, params[:content]) .order(params[:order]) end end
  59. Before 3. the Law of Demeter class Invoice < ActiveRecord::Base belongs_to :user end <%= @invoice.user.name %> <%= @invoice.user.address %> <%= @invoice.user.cellphone %>
  60. 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 %>
  61. 4. DRY: Metaprogramming Before 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
  62. 4. DRY: Metaprogramming After 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
  63. Breaking Up Models Model
  64. Before 5. Extract into Module class User < ActiveRecord::Base validates_presence_of :cellphone before_save :parse_cellphone def parse_cellphone # do something end end
  65. 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
  66. After class User < ActiveRecord::Base include HasCellphone end
  67. Before 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
  68. 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.
  69. 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
  70. 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
  71. Best Practice Lesson 4: Migration
  72. Before 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
  73. After 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
  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. 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 end def self.down drop_table "comments" end end
  76. After 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
  77. Best Practice Lesson 5: Controller
  78. 1. Use before_filter Before 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
  79. 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
  80. Before 2. DRY Controller class PostController < ApplicationController def index def edit @posts = Post.all @post = Post.find(params[:id) end end def show def update @post = Post.find(params[:id) @post = Post.find(params[:id) end @post.update_attributes(params[:post]) redirect_to post_path(@post) def new end @post = Post.new end def destroy @post = Post.find(params[:id) def create @post.destroy @post.create(params[:post] redirect_to posts_path redirect_to post_path(@post) end end 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. 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/
  84. Best Practice Lesson 6: View
  85. Never logic code in Views
  86. 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
  87. 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 %> <% if @post.editable_by?(current_user) %> After <%= 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
  88. 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
  89. 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 } %>
  90. Before 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 %>
  91. After 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 %>
  92. 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
  93. 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 class ApplicationController < ActionController::Base After helper :all # include all helpers, all the time end # app/helpers/posts_helper.rb
  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 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)
  98. Thank you.

×