Rails Best Practices
              ihower@gmail.com




  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...
Ruby Taiwan
  http://ruby.tw
Agenda
• Concept: What’s good code?
• Move Code from Controller to Model
• RESTful best practices
• Model 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 Complex...
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
     h...
Before


        1.Move finder to named_scope
class PostsController < ApplicationController

  def index
    @public_posts ...
After


      1.Move finder to named_scope
class UsersController < ApplicationController

  def index
    @published_post =...
Before


2. Use model association

class PostsController < ApplicationController

  def create
    @post = Post.new(params...
After


      2. Use model association
class PostsController < ApplicationController

  def create
    @post = current_use...
3. Use scope access
                                                Before




class PostsController < ApplicationControll...
3. Use scope access
                                                            After




class PostsController < Applicat...
Before


      4. Add model virtual attribute
<% form_for @user do |f| %>
    <%= text_filed_tag :full_name %>
<% end %>

...
After


4. Add model virtual attribute
    class User < ActiveRecord::Base

      def full_name
        [first_name, last_...
After




<% form_for @user do |f| %>
  <%= f.text_field :full_name %>
<% end %>

class UsersController < ApplicationContr...
5. Use model callback                                Before




<% form_for @post do |f| %>
  <%= f.text_field :content %>...
After


 5. Use model callback
class Post < ActiveRecord::Base

  attr_accessor :auto_tagging
  before_save :generate_tagg...
After




<% form_for :note, ... do |f| %>
  <%= f.text_field :content %>
  <%= f.check_box :auto_tagging %>
<% end

class...
6. Replace Complex Creation                             Before




    with Factory Method
  class InvoiceController < App...
6. Replace Complex Creation
                                                     After




    with Factory Method
  class...
After




class InvoiceController < ApplicationController
  def create
    @invoice = Invoice.new_by_user(params[:invoice]...
7. Move Model Logic into the                       Before




          Model
 class PostController < ApplicationControlle...
7. Move Model Logic into the                 After




          Model
   class Post < ActiveRecord::Base

     def publis...
After




class PostController < ApplicationController

  def publish
    @post = Post.find(params[:id])
    @post.publish...
8. model.collection_model_ids
            (many-to-many)
class User < ActiveRecord::Base

  has_many :user_role_relationsh...
Before


<% form_for @user do |f| %>
  <%= f.text_field :email %>
  <% for role in Role.all %>
    <%= check_box_tag 'role...
After

<% form_for @user do |f| %>

 <% for role in Role.all %>
  <%= check_box_tag 'user[role_ids][]', role.id, @user.rol...
9. Nested Model Forms (one-to-one)               Before




    class Product < ActiveRecord::Base
      has_one :detail
 ...
Before




class Product < ApplicationController

  def create
    @product = Product.new(params[:product])
    @details =...
9. Nested Model Forms (one-to-one)                After


              Rails 2.3 new feature

     class Product < Active...
After




class Product < ApplicationController

  def create
    @product = Product.new(params[:product])
    @product.sa...
10. Nested Model Forms (one-to-many)
      class Project < ActiveRecord::Base
        has_many :tasks
        accepts_nest...
Nested Model Forms
              before Rails 2.3 ?

•   Ryan Bates’s series of railscasts on complex forms
    •   http:/...
Best Practice Lesson 2:


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


class EventsController < ApplicationController



  def index                                def white_member_lis...
After



class EventsController < ApplicationController
  def index; end
  def show; end
end

class   CommentsControlers <...
Before



1. Overuse route customizations

map.resources :posts, :member => { :comments   => :get,
                       ...
After



1. Overuse route customizations
         Find another resources


       map.resources :posts do |post|
         ...
Suppose we has a event model...

   class Event < ActiveRecord::Base

     has_many :attendee
     has_one :map

     has_...
Can you answer how to design
      your resources ?
•   manage event attendees (one-to-many)
•   manage event map (one-to-...
Learn RESTful design
                 my slide about restful:
http://www.slideshare.net/ihower/practical-rails2-350619
Before

            2. Needless deep nesting
                            : Never more than one level




        map.resou...
After

 2. Needless deep nesting
                 : Never more than one level




  map.resources :posts do |post|
    pos...
Before


  3. Not use default route

map.resources :posts, :member => { :push => :post }

map.connect ':controller/:action...
After


        3. Not use default route

map.resources :posts, :member => { :push => :post }

#map.connect ':controller/:...
Best Practice Lesson 3:

   Model
Before

1. Keep Finders on Their Own Model
 class Post < ActiveRecord::Base
   has_many :comments

   def find_valid_comme...
After

1. Keep Finders on Their Own Model
  class Post < ActiveRecord::Base
    has_many :comments
  end

  class Comment ...
Before

                  2. Love named_scope
class PostController < ApplicationController

  def search
    conditions = ...
After

        2. Love named_scope
class Post < ActiveRecord::Base

  named_scope :matching, lambda { |column, value|
    ...
After




class PostController < ApplicationController

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



3. the Law of Demeter
 class Invoice < ActiveRecord::Base
   belongs_to :user
 end

 <%= @invoice.user.name %>
 ...
After



     3. the Law of Demeter
class Invoice < ActiveRecord::Base
  belongs_to :user
  delegate :name, :address, :cel...
4. DRY: Metaprogramming
                                                                         Before



class Post < Ac...
4. DRY: Metaprogramming
                                                               After




class Post < ActiveRecord...
Breaking Up Models
      Model
Before



5. Extract into Module
 class User < ActiveRecord::Base

   validates_presence_of :cellphone
   before_save :par...
After
# /lib/has_cellphone.rb
module HasCellphone

  def self.included(base)
    base.validates_presence_of :cellphone
   ...
After




class User < ActiveRecord::Base

  include HasCellphone

end
Before

6. Extract to composed class
# == Schema Information
# address_city          :string(255)
# address_street        ...
6. Extract to composed class                                                   After



                        (value obj...
Before


             7. Use Observer
class Project < ActiveRecord::Base

  after_create :send_create_notifications

  pri...
After


             7. Use Observer
class Project < ActiveRecord::Base
  # nothing here
end

# app/observers/project_noti...
Best Practice Lesson 4:

Migration
Before


            1. Isolating Seed Data
class CreateRoles < ActiveRecord::Migration
  def self.up
    create_table "ro...
After


         1. Isolating Seed Data

# /db/seeds.rb (Rails 2.3.4)
["admin", "author", "editor","account"].each do |nam...
After




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

namespace :dev do

  desc "Setup seed data"
  task :setup => :enviro...
Before


   2. Always add DB index
class CreateComments < ActiveRecord::Migration
  def self.up
    create_table "comments...
After


   2. Always add DB index
class CreateComments < ActiveRecord::Migration
  def self.up
    create_table "comments"...
Best Practice Lesson 5:

Controller
1. Use before_filter                         Before




class PostController < ApplicationController

  def show
    @post ...
1. Use before_filter                                 After




class PostController < ApplicationController

  before_filte...
Before


                   2. DRY Controller
class PostController < ApplicationController

  def index                   ...
After


       2. DRY Controller
http://github.com/josevalim/inherited_resources

class PostController < InheritedResource...
After


        2. DRY Controller
class PostController < InheritedResources::Base

  # if you need customize redirect url
...
DRY Controller Debate!!

 • You lose intent and readability
 • Deviating from standards makes it harder
    to work with o...
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 p...
2. Move code into model
Before   <% if current_user && (current_user == @post.user ||
                                @pos...
Before     3. Move code into helper
<%= select_tag :state, options_for_select( [[t(:draft),"draft" ],
                    ...
4. Replace instance variable
               with local variable
         class Post < ApplicationController
           def...
Before

        5. Use Form Builder
<% form_for @post do |f| %>

  <p>
     <%= f.label :title, t("post.title") %> <br>
  ...
After

            5. Use Form Builder

<% my_form_for @post do |f| %>

  <%= f.text_field :title, :label => t("post.title...
After
module ApplicationHelper
  def my_form_for(*args, &block)
    options = args.extract_options!.merge(:builder =>
Labe...
6. Organize Helper files
         #   app/helpers/user_posts_helper.rb
Before   #   app/helpers/author_posts_helper.rb
    ...
7. Learn Rails Helpers

• Learn content_for and yield
• Learn how to pass block parameter in helper
 •   my slide about he...
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/r...
Thank you.
Rails Best Practices
Upcoming SlideShare
Loading in...5
×

Rails Best Practices

103,646

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
345 Likes
Statistics
Notes
No Downloads
Views
Total Views
103,646
On Slideshare
0
From Embeds
0
Number of Embeds
40
Actions
Shares
0
Downloads
2,846
Comments
21
Likes
345
Embeds 0
No embeds

No notes for slide

Rails Best Practices

  1. 1. Rails Best Practices ihower@gmail.com As this slide writing, the current Rails version is 2.3.4
  2. 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. 3. Ruby Taiwan http://ruby.tw
  4. 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. 5. Warning! you should have testing before modify!
  6. 6. Best Practice Lesson 0: Concepts
  7. 7. Why best practices? • Large & complicated application • Team & different coding style
  8. 8. Your code become... • (Rigidity) • (Fragility) • (Immobility) • (Viscosity) • (Needless Complexity) • (Needless Repetition) • (Opacity) Agile Software Development: Principles, Patterns, and Practices
  9. 9. We need good code:
  10. 10. What’s Good code? • Readability • Flexibility • Effective • Maintainability • Consistency • Testability
  11. 11. So, What we can do?
  12. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 27. After class InvoiceController < ApplicationController def create @invoice = Invoice.new_by_user(params[:invoice], current_user) @invoice.save end end
  28. 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. 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. 30. After class PostController < ApplicationController def publish @post = Post.find(params[:id]) @post.publish redirect_to post_url(@post) end end
  31. 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. 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. 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. 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. 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. 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. 37. After class Product < ApplicationController def create @product = Product.new(params[:product]) @product.save end end
  38. 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. 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. 40. Best Practice Lesson 2: RESTful RESTful conventions
  41. 41. Why RESTful? RESTful help you to organize/name controllers, routes and actions in standardization way
  42. 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. 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. 44. Before 1. Overuse route customizations map.resources :posts, :member => { :comments => :get, :create_comment => :post, :update_comment => :post, :delete_comment => :post }
  45. 45. After 1. Overuse route customizations Find another resources map.resources :posts do |post| post.resources :comments end
  46. 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. 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. 48. Learn RESTful design my slide about restful: http://www.slideshare.net/ihower/practical-rails2-350619
  49. 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. 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. 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. 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. 53. Best Practice Lesson 3: Model
  54. 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. 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. 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. 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. 58. After class PostController < ApplicationController def search @posts = Post.matching(:title, params[:title]) .matching(:content, params[:content]) .order(params[:order]) end end
  59. 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. 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. 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. 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. 63. Breaking Up Models Model
  64. 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. 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. 66. After class User < ActiveRecord::Base include HasCellphone end
  67. 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. 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. 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. 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. 71. Best Practice Lesson 4: Migration
  72. 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. 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. 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. 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. 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. 77. Best Practice Lesson 5: Controller
  78. 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. 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. 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. 81. After 2. DRY Controller http://github.com/josevalim/inherited_resources class PostController < InheritedResources::Base # magic!! nothing here! end
  82. 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. 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. 84. Best Practice Lesson 6: View
  85. 85. Never logic code in Views
  86. 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. 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. 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. 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. 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. 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. 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. 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. 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. 95. Best Practice Lesson 7: Code Refactoring
  96. 96. We have Ruby edition now!! Must read it!
  97. 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. 98. Thank you.
  1. A particular slide catching your eye?

    Clipping is a handy way to collect important slides you want to go back to later.

×