Hatena::ブログ(Diary)

わからん

2012.09.04

[] RailsCast #286 Draper を active_decorator で書き直してみて得たこと

RailsCast の #286 で紹介されている Draper を active_decorator gem で書き直してみました。この回は、日本語版の AsciiCast でも読むことができます。作業自体は単調で難易度も低く面白くありませんでしたが、RailsCast #286 Draper の精読も含めると、以下のような点を学べました。

  • RailsCast に載っている程度の例であれば、Draper も active_decorator もほとんど違いはない。Draper では作成したデコレータの公開範囲を指定できるとわかるが、たいして重要な機能ではないと思う
  • デコレータを導入することで、モデルにビューのためのヘルパーメソッドを書かずにすみ、うまく分業できる
  • デコレータを導入することで、ビューでの条件分岐をいくつかうまく隠蔽できる。もっともそれはヘルパー(app/helpers)でも実現できていた
  • デコレータを導入し、ヘルパーからモデルに由来するメソッドをデコレータに移動させることで、ヘルパーにあったメソッドを、よりオブジェクト指向的に捉え直すことができる

RailsCast #286 Draper を active_decorator で書き直してみるだけでは次のことがまだわかりません。

  • draper と active_decorator の違い。draper がクラスベースで active_decorator がモジュールベースというのはわかるが、そのことで何か致命的な違いが生じるのか

こちらはブログにしたい気分になったらいつか書きます(asakusa.rb で教わった小ネタです)。以下は作業ログです。文章は ASCIIcasts の “Episode 286 - Draper” からそのまま引用しているところがけっこうあります。要点は2つで、1つは、draper ではデコレータで model. とか h. とか付ける必要があるけど、active_decorator では不要であること。もう1つは、active_decorator ではカラム名と同名のデコレータメソッドを作成できないことです。



Userモデルに対してdecoratorを作成します。以下は active_decorator の例です。

$ rails g decorator user
      create  app/decorators/user_decorator.rb
      invoke  test_unit
      create    test/decorators/user_decorator_test.rb

app/decorators/user_decorator.rb は空のモジュールです。

  # coding: utf-8
  module UserDecorator
  end

 プロフィールページを編集する

Draper では、プロフィールページを編集するのに、UsersController 内の show アクションを次のように修正する必要がありますが、active_decorator ではその必要がありません。

def show
-  @user = User.find(params[:id])
+  @user = UserDecorator.find(params[:id]) # active_decorator ではこの変更は不要
end

それではビューの整理を始めますが、まずはユーザのアバターを表示するコードを修正します。

<%= link_to_if @user.url.present?, image_tag("avatars/#{avatar_name(@user)}", class: "avatar"), @user.url %>

ビューのこの部分を次のように置き換えます。

<%= @user.avatar %>

▼ draper

  class UserDecorator < ApplicationDecorator
    decorates :user

    def avatar
      h.link_to_if model.url.present?, h.image_tag("avatars/#{avatar_name}", class: "avatar"), model.url
    end

    private
    def avatar_name
      if model.avatar_image_name.present?
        model.avatar_image_name
      else
        "default.png"
      end
    end
  end

▼ active_decorator

module UserDecorator

  def avatar
    link_to_if url.present?, image_tag("avatars/#{avatar_name}", class: "avatar"), url
  end

  private
  def avatar_name
    if avatar_image_name.present?
      avatar_image_name
    else
      "default.png"
    end
  end
end

次にユーザ名を表示するコードを整理します。ビューのこのコードを置き換えます。

<h1><%= link_to_if @user.url.present?, (@user.full_name.present? ? @user.full_name : @user.username), @user.url %></h1>

これを次のように書き換えます。

<h1><%= @user.linked_name %></h1>

デコレータは次のようになります。avatar_name のときとほぼ同様です。


▼ draper

  def linked_name
    site_link(model.full_name.present? ? model.full_name : model.username)
  end

  private
  def site_link(content)
    h.link_to_if model.url.present?, content, model.url
  end

▼ active_decorator

  def linked_name
    site_link(full_name.present? ? full_name : username)
  end

  private

  def site_link(content)
    link_to_if url.present?, content, url
  end

テンプレートは十分きれいになったようですが、まだ改善の余地があります。次はビューコードの中のより大きな部分をリファクタリングします。ユーザのWebサイトへのリンクを表示する部分のコードです。

  <dt>Website:</dt>
  <dd>
    <% if @user.url.present? %>
      <%= link_to @user.url, @user.url %>
    <% else %>
      <span class="none">None given</span>
    <% end %>
  </dd>

これを下の内容で置き換えます。

  <dt>Website:</dt>
  <dd><%= @user.website %></dd>

前と同じようにメソッドをデコレータに作成します。


▼ draper

  def website
    if model.url.present?
      h.link_to model.url, model.url
    else
      h.content_tag :span, "None given", class: "none"
    end
  end

▼ active_decorator

  def website
    if url.present?
      link_to url, url
    else
      content_tag :span, "None given", class: "none"
    end
  end

Twitterの情報とユーザの略歴をそれぞれ表示するテンプレートの部分についても同じ手法をとることができます。

  <dt>Twitter:</dt>
  <dd>
  <% if @user.twitter_name.present? %>
    <%= link_to @user.twitter_name, "http://twitter.com/#{@user.twitter_name}" %>
  <% else %>
    <span class="none">None given</span>
  <% end %>
  </dd>
  <dt>Bio:</dt>
  <dd>
  <% if @user.bio.present? %>
    <%=raw Redcarpet.new(@user.bio, :hard_wrap, :filter_html, :autolink).to_html %>
  <% else %>
    <span class="none">None given</span>
  <% end %>
  </dd>

を次のようにします。

  <dt>Twitter:</dt>
  <dd><%= @user.twitter %></dd>
  <dt>Bio:</dt>
  <dd><%= @user.bio %></dd>

デコレータは次のように書きます。


▼ draper

  def twitter
    if model.twitter_name.present?
      h.link_to model.twitter_name, "http://twitter.com/#{model.twitter_name}"
    else
      h.content_tag :span, "None given", class: "none"
    end
  end

  def bio
    if model.bio.present?
      Redcarpet.new(model.bio, :hard_wrap, :filter_html, :autolink).to_html.html_safe
    else
      h.content_tag :span, "None given", class: "none"
    end
  end

▼ active_decorator


  # ビューへの変更
  -    <dd><%= @user.deco %></dd>
  +    <dd><%= @user.deco_bio %></dd>


  def twitter
    if twitter_name.present?
      link_to twitter_name, "http://twitter.com/#{twitter_name}"
    else
      content_tag :span, "None given", class: "none"
    end
  end

  def deco_bio       # bio というメソッド名は重複するため使えない
    if bio.present?
      Redcarpet.new(bio, :hard_wrap, :filter_html, :autolink).to_html.html_safe
    else
      content_tag :span, "None given", class: "none"
    end
  end

website(), twitter(), bio()/deco_biot() の else 節の重複を独立したメソッドとして抽出します。draper の場合のコードは省略します。

  def website
    handle_none url do
      link_to url, url
    end
  end

  def twitter
    handle_none twitter_name do
      link_to twitter_name, "http://twitter.com/#{twitter_name}"
    end
  end

  def deco_bio
    handle_none bio do
      Redcarpet.new(bio, :hard_wrap, :filter_html, :autolink).to_html.html_safe
    end
  end

  private

  def handle_none(value)
    if value.present?
      yield
    else
      content_tag :span, "None given", class: "none"
    end
  end

もう一つ修正できる点として、Markdownの表示処理をApplicationDecoratorに抽出して、今後作るかもしれない他のdecoratorから呼び出せるようにします。新たにmarkdownメソッドを作成し、そこで渡されたテキストを表示処理します。draper では Draper::Base を継承した ApplicationDecorator クラスを作成しますが、active_decorator では ApplicationDecorator モジュールを作成し、それを include すれば同様の振る舞いを実現できます。


▼ active_decorator / application_decorator.rb

  # coding: utf-8
  module ApplicationDecorator
    def markdown(text)
      Redcarpet.new(text, :hard_wrap, :filter_html, :autolink).to_html.html_safe
    end
  end

▼ active_decorator / user_decorator.rb

  module UserDecorator

    include ApplicationDecorator

    ...

    # def deco_bio
    #   handle_none bio do
    #     Redcarpet.new(bio, :hard_wrap, :filter_html, :autolink).to_html.html_safe
    #   end
    # end

    def deco_bio
      handle_none bio do
        markdown bio
      end
    end

 モデルを修正する

decoratorが正しく機能するように設定できたので、ここでモデル層を一度見渡してみて、もしビュー関連のコードがあったらそれを対応するdecoratorに移動させます。例えば、Userモデルにはユーザが作成された時間をフォーマットするmember_sinceメソッドがあります。このコードは、フォーマットされた文字列を返すだけなのでビュー関連だと見なされます。これをdecoratorに移動します。


▼ モデル /app/models/user.rb

  class User < ActiveRecord::Base
    # def member_since
    #   created_at.strftime("%B %e, %Y")
    # end
  end

▼ デコレータ /app/decorators/user_decorator.rb

  def member_since
    created_at.strftime("%B %e, %Y") #draper の場合はレシーバに model を指定するだけの違い
  end

draper では allows メソッドを用いてモデルへのアクセスを制限することができます。active_decorator にこの機能はありません。

  class UserDecorator < ApplicationDecorator
    decorates :user
    allows :username

    # Other methods omitted
  end

 参考


 追記(2012/9/19)

sapporo rubykaigi 2012 の Rails3 Recipe Book Gaiden に、ActiveDecorator で 関連先を decorate したいときにどうすかの資料がありました。

 

はてなユーザーのみコメントできます。はてなへログインもしくは新規登録をおこなってください。

Google