Link Search Menu Expand Document

Part 1: 關於 Controller

2. 將代碼從 Controller 重構到 Model

Skinny Controller, Fat Model 是一篇在 Rails 社區非常有名的文章,這篇 2006 年 Rails 2 時期的舊文章,定調了 Fat Controller 是一種反模式(anti-pattern)。放在 Controller 的代碼一般來說比較難進行重用(re-use)和單元測試,可讀性也較差,我們希望將更多代碼放在 Model 裡面。

一個 Rails 專案如果 controller 代碼很多,但是 model 代碼打開卻沒有自定義什麽方法,給人的印象上就是這是軟體開發初學者所寫的專案。

基本上,一段 Controller 的 action 代碼如果超過15行以上,很可能這段代碼表示太肥大了,應該進行重構。

以下示範幾個情境,適合將 Controller 的代碼重構到 Model,目的都是在簡化 controller 的代碼。

3. 善用 Model scope

情境:在 action 中需要依照不同條件撈取 Model 數據

重構前:在 action 中用 where 指定不同條件

class PostsController < ApplicationController
  def index
    @public_posts = Post.where(:state => 'public' ).limit(10)
                  .order('id desc')
    @draft_posts  = Post.where(:state => draft').limit(10)
                  .order('id desc')
  end
end

重構後:把 where 條件搬到 model 的 scope 宣告,這樣就可以在 controller 使用已經定義好的 scope。可讀性變好,而且可以在不同地方重復沿用這個 scope。

class Post < ActiveRecord::Base
  scope :published, -> { where(:state => published').limit(10)
                        .order('id desc') }
  scope :draft, -> { where(:state => ‘draft').limit(10)
                    .order('id desc') }
end
class UsersController < ApplicationController
  def index
    @published_post = Post.published
    @draft_post = Post.draft
  end
end

4. 善用 Model association

情境:新建數據時,想要關聯建立的用戶

重構前:

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

重構後:由於 User has_many posts 的關系,我們可以用 current_user.posts.build 來取代 @post.user_id = current_user.id 的作用。

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

5. 有關聯的權限檢查 scope access

情境:讀取數據時,想要檢查用戶有沒有操作該數據的權限

重構前:需要檢查 @post.user 等於 current_user

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

重構後:直接用 current_user.posts.find 就可以了。如果該 post 不屬於該 user,就會找不到數據。不過,如果權限允許管理員的話,這招就不行了。

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

6. 使用 Model 虛擬屬性

情境:在表單中,操作不是直接對應 Model 屬性的欄位。例如下述範例,假設 User model 裡面有 first_namelast_name 欄位,但是畫面顯示時,我們希望改用 full_name 來操作。這個 full_name 並不是資料庫中真正的欄位,而是 first_name 和 last_name 兩個欄位合在一起顯示而已。

重構前:表單只能用原始的 text_field_tag 方法,並且在 action 中拆開 params[:full_name] 塞進 model 的 first_name 和 last_name 欄位

<% 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

重構後:在 model 中新增 full_namefull_name= 方法,這樣在表單就可以把 full_name 當作一般的 model 欄位使用。而 controller action 更是簡化到不需要處理 full_name。這個 full_name 並沒有實際對應資料庫的欄位,因此稱之虛擬屬性。

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
<% form_for @user do |f| %>
  <%= f.text_field :full_name %>
<% end %>
class UsersController < ApplicationController

  def create
    @user = User.create(params[:user)
  end

end

7. 使用 Model 回呼(callback)

情境:新增文章時,有一個核選方塊 auto_tagging,如果打勾表示想要系統自動下標籤。

重構前:需要在 action 中檢查 params[:auto_tagging],然後調用 model 方法產生標籤(這裡假設有一個 AsiaSearch.generate_tags 方法可以自動下標籤)

<% 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

重構後:新增一個虛擬屬性 auto_tagging,以及一個 before_save 回呼來檢查要不要自動下標籤。如此在 action 中就可以不必檢查和處理 auto_tagging。這段過程會自動在 Post 存儲前,自動調用 generate_taggings 方法進行處理。

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
<% 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

8. 將邏輯放到 Model

情境:有一個發布文章 publish 的 action 有很多操作的相關步驟,需要設定很多 model 欄位。

重構前:所有操作都在 action 之中

class PostController < ApplicationController
 def publish
   @post = Post.find(params[:id])
   @post.is_published = true
   @post.approved_by = current_user

   if @post.create_at > Time.now - 7.days
     @post.popular = 100
   else
     @post.popular = 0
   end

   @post.save
   redirect_to post_url(@post)
  end
end

重構後:把相關的操作全部搬到 Model 的自定義方法 publish!,這樣 action 中只需要調用 @post.publish! 即可,非常可讀清楚。這個 publish! 方法也可以在其它各處使用。

class Post < ActiveRecord::Base
  def publish!(user)
    self.is_published = true
    self.approved_by = user
    if self.create_at > Time.now-7.days
      self.popular = 100
    else
      self.popular = 0
    end
    self.save!
  end
end
class PostController < ApplicationController
  def publish
    @post = Post.find(params[:id])
    @post.publish!(current_user)

    redirect_to post_url(@post)
  end
end

9. 使用工廠方法(Factory Method)取代複雜的建構過程

情境:建立 model 需要複雜的建構過程,例如以下建構 Invoice 需要設定很多屬性。

重構前:都在 create action 中完成

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

重構後:在 model 中新寫一個類方法 new_by_user,把建構的過程全部搬進來。這樣 controller action 裡面只需要調用這個方法即可。這一招跟上一節是一樣的道理。這種建構用途的方法又叫做工廠方法。

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
    return invoice
  end
end
class InvoiceController < ApplicationController
  def create
    @invoice = Invoice.new_by_user(params[:invoice], current_user)
    @invoice.save
  end
end

Copyright © 2010-2022 Wen-Tien Chang All Rights Reserved.