over 3 years ago

這一篇的重點也是 Object-Oriented View。因為篇幅太長所以再拆一篇。

16. Form Builder

有時候我們為了排版 Form,不得不在 Form 裡面也穿插一些 HTML 作 styling。

<%= form_for @user do |form| %>
  <div class="field">
    <%= form.label :name %>
    <%= form.text_field :name %>
  </div>

  <div class="field">
    <%= form.label :email %>
    <%= form.text_field :email %>
  </div>
<% end %>

但要寫十幾遍 <div class="field"> 是一件很煩人的事。我們最希望的是,其實 View 裡面只要這樣寫就 OK 了:

<%= form_for @user, :builder => HandcraftBuilder do |form| %>
  <%= form.custom_text_field :name %>
  <%= form.custom_text_field :email %>
<% end %>

這樣的煩惱可以透過客製 Form Builder 解決:

class HandcraftBuilder < ActionView::Helpers::FormBuilder
  def custom_text_field(attribute, options = {})
    @template.content_tag(:div, class: "field") do
      label(attribute) + text_field(attribute, options)
    end
  end
end

其他 Form Builder

不過現在還需要自己寫 Form Builder 嗎?其實機會蠻少了。主要的原因是如熱門的 Framework:Bootstrap 有專屬的 gem bootstrap_form。而 simple_form 也提供 template,透過 API 就可以輕鬆客製出一個 Form Builder。

17. Form Object (wrap logic in FORM, not in model nor in controller)

Form Object 是一個比較新的概念。它的想法是,其實表單的邏輯驗證不應該發生在 Model 裡面也不應該發生在 Controller 裡面。

我們可以重新設計一個 Form Object,使用 ActiveModel 的部份 API 將邏輯重新包裝,塞進 Form Builder 裡面:

( 詳細手法可以見這篇文章:Form-backing objects for fun and profit )

class Forms::Registration

  # ActiveModel plumbing to make `form_for` work

  extend ActiveModel::Naming
  include ActiveModel::Conversion
  include ActiveModel::Validations

  def persisted?
    false
  end
  
 .....
end

這巧妙的解決了一些問題。比如讓人很煩的 massive assignment issue( 其實使用 strong_parameter 也會讓人心情煩躁)。而且 strong_parameter 並沒有辦法解決這樣的問題:

<%= simple_form_for @registration, :url => registrations_path, :as => :registration do |f| %>

  <%= f.input :name %>
  <%= f.input :email %>
  
  <label class="checkbox">
    <%= check_box_tag :terms_of_service %>
    I accept the <%= link_to("Terms of Service ", "/pages/tos") %>
  </label>
  <%= f.submit %>
<% end %>

有時候我們必須要在註冊表單上,多加一個 check_box,確認使用者同意註冊條款。而 controller 就會變得這麼噁心。

def create
  if params[:terms_of_service]
    if @regfistration.save
      redirect_to root_path
    else
      render :new
    end
  else
    render :new
  end
end

而這麼噁心的 controller 如果又再加上 captcha 或是一些客製選項,那就又會變得更恐怖了。不過 Form Object 的設計門檻也不是很低。

所以 cells 的作者又推出了這麼一個 Gem : Reform,簡化 Form Object 的包裝。

Reform (Decouples your models from form validation, presentation and workflows.)

透過 Reform,剛剛的 Logic 可以被簡化成:

class RegistrationForm < Reform::Form
  property :name
  property :email
  property :term_of_service

  validates :term_of_service, :presence => true
end

而 controller 裡面又可以重新變會成漂漂亮亮的一層 if/else :

def create
  if @form.validate(params[:registration])
    @form.save
  else
    render :new
  end
end

18. Policy Object / Rule Engine (centralize permission control)

這是這一個系列的最後一招。在設計 Application 的時候,我們常要面對權限的設計封裝問題,如:

def render_post_edit_option(post)
  if post.user == current_user
    render :partial => "post/edit_bar"
  end
end

當權限只有 current_user 時還沒有什麼問題。不過權限通常是會膨脹下去的:

def render_post_edit_option(post)
  if post.user == current_user || current_user.admin? 
    render :partial => "post/edit_bar"
  end
end

多一個 admin? 還不打緊,但事情往往沒那麼簡單,過不久可能又會生出一個 moderator?

def render_post_edit_option(post)
  if post.user == current_user || current_user.admin? || current_user.moderator?
    render :partial => "post/edit_bar"
  end
end

整串邏輯就變得又臭又長。最麻煩的是除了 View 之外,Controller 其實也是需要配合權限檢查的:

class PostController < ApplicationController
  before_filter :check_permission, :only => [:edit]
  
  def edit
    @post = Post.find(params[:id])
  end
end

Cancan (Authorization Gem for Ruby on Rails)

cancan 是最常被想到的一個整理的招數。透過 Rule Engine 的結構,整理權限:

<% if can? :update, @post %>
   <%= render :partial => "post/edit_bar" %>
<% end %>
class Ability
  include CanCan::Ability

  def initialize(user)

    if user.blank?
      # not logged in

      cannot :manage, :all
    elsif user.has_role?(:admin)
      can :manage, :all
    elsif user.has_role?(:moderator)
      can :manage, Post
    else
      can :update, Post do |post|
        (post.user_id == user.id)
      end
    end
  end
end

我之前曾經寫過一個 Cancan 系列,如果你有興趣深入把玩 Cancan 的話,以下是系列連結:

Pundit (Minimal authorization through OO design and pure Ruby classes)

不過 cancan 這種 Rule Engine 式的設計常被開發者嫌過度笨重。最近還新誕生了一種設計手法,利用 Policy Object 對於權限進行整理,其中有一個 gem : pundit 算做得蠻不錯的。

Pundit 的想法是把單獨的一組 logic 抽取出來,放在 app/policies 下。

class PostPolicy
  attr_reader :user, :post

  def initialize(user, post)
    @user = user
    @post = post
  end

  def edit?
    user.admin? || user.moderator?
  end
end

而在 View 裡面單獨使用 policy object 驗證:

<% if policy(@post).edit? %>
  <%= render :partial => "post/edit_bar" %>
<% end %>

controller 裡面也只要 include Pundit,就可以套用邏輯。

class ApplicationController < ActionController::Base
  include Pundit
  protect_from_forgery
end


Maintainable Rails View 系列文目錄

← Maintainable Rails View (4) Maintainable Rails View (6) →
 
comments powered by Disqus