Thumb resize

[Meta Programming]手刻 Restful DRY Controller

by 鄧慕凡 2017-10-13 04:49 UTC

===

本篇文章將指引如何透過手刻共用的 Restful Resource Controller 的方式來達到在 Rails 的 Controller 程式碼中 DRY(Don’t Repeat Yourself)的目的。

寫 Rails 的人通常都知道什麼是 Restful,也都會用 Restful 的方式寫大部份的 CRUD 操作,以一般的 CRUD 操作來說,我們的 Controller 都會長的像下面這樣:

class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]
  def index
    @posts = Post.all
  end
  def show
  end
  def new
    @post = Post.new
  end
  def edit
  end
  def create
    @post = Post.new(post_params)
    if @post.save
      redirect_to @post, notice: 'Post was successfully created.'
    else
      render :new
    end
  end
  def update
    if @post.update(post_params
      redirect_to @post, notice: 'Post was successfully updated.'
    else
      render :edit
    end
  end
  def destroy
    @post.destroy
    redirect_to posts_url, notice: 'Post was successfully destroyed.'
  end
private
  def set_post
    @post = Post.find(params[:id])
  end
  def post_params
    params.require(:post).permit(:subject, :content, :slug, :author)
  end
end

在我們開發 Rails 應用程式的過程中常常會需要重複這個過程,例如 Controller 是 ArticlesController 的話,上面的 @post 就會變成 @article… 以下類推。

在這邊介紹一下筆者以前常用的,可以重用的 Controller 寫法,目標是讓所有 Restful CRUD 操作都不用重新寫類似的 Controller Code。

class ResourcesController < ApplicationController
  helper_method :model_class, :model_class_name, :current_collection, :current_object
  def index
  end

  def new
    @current_object = default_scope.new
  end

  def create
    if @current_object = default_scope.create(permitted_attributes)
      redirect_to action_after_create
    else
      render action: :new
    end
  end

  def update
    if current_object.update(permitted_attributes)
      redirect_to action_after_update
    else
      render action: :edit
    end
  end

  def destroy
    current_object.destroy
    redirect_to action_after_destroy
  end

protected

  def model_class_name #may need to re-define in your controller
    self.class.to_s.demodulize.gsub('Controller', '').singularize
  end

  def model_class #may need to re-define in your controller
    model_class_name.constantize
  end

  def permitted_attributes
    #must re-define in your controller
  end

  def param_key
    model_class_name.underscore.to_sym
  end

  def default_scope
    model_class
  end

  def current_collection
    @current_collection ||= default_scope.page(params[:page])
  end

  def current_object
    @current_object ||= default_scope.find(params[:id])
  end

  def object_params
    params.require(param_key).permit(*permitted_attributes)
  end

  def action_after_create
    url_for action: :index
  end

  def action_after_update
    request.referrer || url_for(action: :index)
  end
  alias :action_after_destroy :action_after_update

end

應用方式

當你的 Controller 繼承了這個 class 之後,最小限的狀況下,在一般的 CRUD 操作下只需重新定義以下的部份(以下假設程式中有 Page 這個 Model):

class PagesController < ResourcesController
  protected
  def permitted_attributes
    %w{page_title subdn host_name contact_email will_expire_at}.freeze
  end
end

而在 View 裡則必需將原本的 @pages 集合改成 current_collection 這個 helper method,@page 物件改成current_object 即可。

如果需要自訂新增/修改/刪除成功後的重導向行為,只要重定義對應的 actionafterxxx method 即可。

實作思路

以下就每個共用 method 來解說實作方法:

  helper_method :model_class, :model_class_name, :current_collection, :current_object
  # 將以上 method 變成 view 中也可以使用的狀況

  def model_class_name
    # 將 Controller 的名稱去除可能的 namespace 後再拿掉 Controller 的字串,然後再單數化
    self.class.to_s.demodulize.gsub('Controller', '').singularize
  end

  def model_class
    # 將前面取得的 model_class_name 字串常數化
    model_class_name.constantize
  end

  def param_key
    # 將前面的 model_class_name 底線字串化做為預設的 form namescope
    # Page -> page
    model_class_name.underscore.to_sym
  end

  def default_scope
    # 預設狀態下是 Class 本身,可以視情況自行重定義調整
    model_class
  end

  def current_collection
    # 由前面的 default_scope 限制範圍再加上 Kaminari 提供的 page method 做分頁
    @current_collection ||= default_scope.page(params[:page])
  end

  def current_object
    # 用 default_scope 限制搜尋的範圍
    @current_object ||= default_scope.find(params[:id])
  end

  def object_params
    # 這邊需要在每個繼承後的 controller 內定義 permitted_attributes
    params.require(param_key).permit(*permitted_attributes)
  end

變化形–加入身份認證與 scope

通常在有 User 要管理自己所屬的資源時,我們需要在 Controller 取得資料時加上特定的 scope 去限制搜尋的範圍,這時可以用以下的多重繼承方式達成(以下假設使用 Devise Gem 做身份認證):


# in app/controllers/panel/resources_controller.rb
class Panel::ResourcesController < ::ResourcesController
  layout 'panel'
  before_action :authenticate_member!
protected

  def relation_name
    param_key.pluralize
  end

  def default_scope
    current_member.send(relation_name)
  end
end

# in app/controllers/panel/pages_controller.rb
class ::Panel::PagesController < ::Panel::ResourcesController
  def permitted_attributes
    %w{page_title subdn host_name contact_email will_expire_at}.freeze
  end
end

小結

如果你常常要寫很多 Restful Resources CRUD Controller 的話,那這個方式應該可以節省不少時間,雖然 RubyGems 上有類似的 Gem 例如 restful_controller 之類,不過以我個人的經驗,會建議最好實作出自己的版本並實踐幾次後再去考慮使用 Gem 的話,技能會成長的比較快。

Photo by Shane Willis

«回文章列表