homeASCIIcasts

259: Decent Exposure 

(view original Railscast)

Other translations: En Es

Other formats:

Written by Naomi Fujimoto

今回のエピソードでは、decent_exposureというgemを紹介します。このgemはシンプルですが優れたコンセプトを持っています。これを使うことによって、インスタンス変数を使わずに、ビューからアクセスできるメソッドのインターフェイスをコントローラ内に作成できます。このgemは、exposeというメソッドを使ってこのインターフェイスを定義します。

decent_exposureを見る前に、手作業でこのコンセプトを実装してみましょう。対象とするのは簡単なブログアプリケーションで、複数のArticles(記事)とそれに対する複数のComments(コメント)からなります。

簡単なブログアプリケーション

ArticlesControllerには、標準的なコントーラのコードが含まれています。たとえばindexアクションでは、@articlesというインスタンス変数を生成します。

/app/controllers/articles_controller.rb

class ArticlesController < ApplicationController
  def index
    @articles = Article.order(:name)
  end

  #Other actions omitted.
end

indexビューのコードでは、@articlesを用いて各記事を繰り返し読み込んで表示しています。

最初にRailsを使い始めた頃、コントローラのインスタンス変数へのアクセスがビューと共有されていることに違和感を覚えなかったでしょうか? 通常それらはクラス内のプライベート変数となっているべきではないかと。そこで今回は、ビューに対してメソッドを公開することでデータを共有する、もう一つのアプローチを見ていきます。これを実現するために、単独のモデルかモデルのリストを返すプライベートメソッドを、コントローラ内に作成します。

まずindexアクションから、複数のarticleを取得するコード行を削除して、それをarticlesというメソッドに貼付けます。@articlesをキャッシュとして利用し、記事のリストが一度だけしか取得されないようにします。そのために||=演算子を使用し、コントーラやビューの他の場所ではこのインスタンス変数を参照しないようにします。articlesをヘルパーメソッドにして、ビューで使えるようにします。

/app/controllers/articles_controller.rb

class ArticlesController < ApplicationController
  def index
  end

  private
  def articles
    @articles ||= Article.order(:name)
  end
  helper_method :articles
end

ビューでは、インスタンス変数の呼び出しを新しく作ったarticlesメソッドに置き換えます。

/app/views/articles/index.html.erb

<% title "Articles" %>

<div id="articles">
<% for article in articles %>
  <h2>
    <%= link_to article.name, article %>
    <span class="comments">(<%= pluralize(article.comments.size, 'comment') %>)</span>
  </h2>
  <div class="created_at">on <%= article.created_at.strftime('%b %d, %Y') %></div>
  <div class="content"><%= simple_format(article.content) %></div>
<% end %>
</div>

<p><%= link_to "New Article", new_article_path %></p>

コントーラ内の他のアクションで個別の記事を検索したり新規作成しますが、articleメソッドを作ることでこれと似たことができます。これは、articlesメソッドよりも少し複雑になります。というのも、渡されるパラメータによって違う動作をしなければいけないからです。メソッドはこのような形になります。

/app/controllers/articles_controller.rb

def article
  @article ||= params[:id] ?Article.find(params[:id]) : Article.new(params[:id])
end
helper_method :article

もしidパラメータが存在すれば、メソッドはそのidArticleを探します。なければ、articleパラメータの内容に基づいて新しい記事を作成します。@articleインスタンス変数をこの新しく作ったメソッドに置き換えることによって、記事を検索したり新規作成する行をアクション内からなくすことができます。

/app/controllers/articles_controller.rb

class ArticlesController < ApplicationController
  def index
  end

  def show
  end

  def new
  end
  
  def create
    if article.save
      redirect_to articles_path, :notice => "Successfully created article."
    else
      render :new
    end
  end

  def edit
  end
  
  def update
    if article.update_attributes(params[:articles])
      redirect_to articles_path, :notice => "Successfully updated article."
    else
      render :edit
    end
  end
  
  def destroy
    @article.destroy
    redirect_to articles_url, :notice => "Successfully destroyed article."
  end

private
  def articles
    @articles ||= Article.order(:name)
  end
  helper_method :articles
  
  def article
    @article ||= params[:id] ?Article.find(params[:id]) : Article.new(params[:id])
  end
  helper_method :article
end

アクションのいくつかはコードがなくなりました。これは唯一行っていたインスタンス変数の定義が、actionメソッドで処理されるようになったからです。この後、ArticleControllerの各ビュー内でインスタンス変数を使用していた部分を、対応するメソッドに置き換える必要があります。例えばshowビューは修正後は次のようになります。

/app/views/articles/show.html.erb

<% title article.name %>

<%= simple_format article.content %>

<p>
  <%= link_to pluralize(article.comments.size, 'Comment'), [article, :comments]%> |
  <%= link_to "Back to Articles", articles_path %> |
  <%= link_to "Edit", edit_article_path(article) %> |
  <%= link_to "Destroy", article, :method => :delete, :confirm => "Are you sure?" %>
</p>

他のビューについては省略しますが、同じような修正が必要です。

このアプローチのもう一つの利点は、読み込みが必要最小限になることです。例えば、showアクションにアクションキャッシュを追加する場合、ビューのレンダリング時に表示される記事のみがデータベースから取り出され、コントローラ層では要求されません。これによって、コントローラが本当に必要とするまではアクションが要求されなくなるので、アクションキャッシュが効率的におこなわれます。

decent_exposure gemを追加する

便利な機能を手に入れることができましたが、ビューからアクセスできるメソッドをより簡単に定義できればさらに便利でしょう。ここでdecent_exposureの登場です。このgemのexposeメソッドを使って、前半で作ったarticlesarticleメソッドと似た方法でビューに対してモデルにアクセスする手段を提供できるようになります。exposeメソッドは、デフォルト設定で以下のように動作します。idパラメータでモデルを探し、もしそれが見つからなければ、見つけられた適当なパラメータを用いて新しいモデルを作成します。つまり、single modelsの検索や作成にはデフォルト値を使うことができます。別の動作が必要であれば、メソッドにブロックを渡してその中に定義します。キャッシュの処理は、decent_exposureが自動でおこなってくれます。

ではこれをアプリケーションに組み込んでみましょう。まずGemfileにgemを追加し、bundleコマンドを実行します。

/Gemfile

source 'http://rubygems.org'

gem 'rails', '3.0.5'
gem 'sqlite3'
gem 'nifty-generators'
gem 'decent_exposure'

これでArticlesController内に書いたarticlearticlesの各メソッドを、2つのexposeの呼び出しに置き換えることができます。

/app/controllers/articles_controller.rb

class ArticlesController < ApplicationController

  expose(:article)
  expose(:articles) { Article.order(:name) }
  
  def index
  end
  
  # Other actions omitted
end

exposeのデフォルト設定は個々のArticleの処理にはそのまま利用できますが、Article(記事)のリストがほしい場合は動作をカスタマイズする必要があるので、articlesメソッドからその部分をコピーしてexposeのブロックに貼り付けます。

アプリケーションを再度読み込むと、以前と同じように動作します。インスタンス変数の代わりにdecent_exposureが提供するメソッドを利用しているので、コントローラはきれいに整理されました。

ネストされたリソースを取り扱う

decent_exposureはネストされたリソース(例えば今回のアプリケーションの場合の、記事の下にぶら下がったコメント)をどう取り扱うのでしょうか?

/config/routes.rb

Blog::Application.routes.draw do
  root :to => "articles#index"

  resources :articles do
    resources :comments
  end
end

CommentsControllerは次のような形になります。

/app/controllers/comments_controller.rb

class CommentsController < ApplicationController
  def index
    @article = article.find(params[:article_id])
    @comments = @article.comments
    @comment = Comment.new
  end
  
  def new
    @article = Article.find(params[:article_id])
    @comment = @article.comments.build
  end
  
  def create
    @article = Article.find(params[:article_id])
    @comment = @article.comments.build(params[:comment])
    if @comment.save
      redirect_to @comment.article, :notice => "Successfully created comment!"
    else
      render :new
    end
  end
end

まだここではインスタンス変数が使われています。各アクションの最初で記事を取得し、その記事を介してコメントを取得するか作成します。decent_exposureはネストされたリソースをサポートしているので、それをここで利用します。

前半と同じように、インスタンス変数をexposeの呼び出しに置き換えます。個々のArticleCommentの取得にはデフォルトの動作をそのまま使えますが、コメントのリストを取得するには動作のカスタマイズが必要です。コントローラでインスタンス変数を設定しているコード行を削除し、残ったコードのうちのインスタンス変数の部分を対応するメソッドの呼び出しに置き換えます。これらの修正をおこなうとコントローラはきれいに整理されました。

/app/controllers/comments_controller.rb

class CommentsController < ApplicationController
  expose(:article)
  expose(:comments) { article.comments }
  expose(:comment)
  
  def index
  end
  
  def new
  end
  
  def create
    if comment.save
      redirect_to comment.article, :notice => "Successfully created comment!"
    else
      render :new
    end
  end
end

ArticlesControllerを修正したときと同じように、このコントローラに関連するビューを書き直して、インスタンス変数の代わりに、decent_exposureに生成されたメソッドを呼び出すように修正します。次に示すフォーム部品ファイルを参照してください。

/app/views/comments/_form.html.erb

<%= form_for [article, comment] do |f| %>
  <%= f.error_messages %>
  <%= f.hidden_field :article_id %>
  <p>
    <%= f.label :name %>
    <%= f.text_field :name %>
  </p>
  <p>
    <%= f.label :content, "Comment" %><br />
    <%= f.text_area :content, :rows => 12, :cols => 35 %>
  </p>
  <p><%= f.submit %></p>
<% end %>

アプリケーションを操作してみると、動作は以前と同じですが、コントローラのコードは大きく改善されました。

decent_exposureを利用する場合に、ひとつ気をつけなくてはいけないことがあります。exposeをデフォルト設定で個々のモデルを取得するのに用いる場合、渡される名前(:article)の複数形を探し、存在する場合はそれを取得してそのスコープでレコードを組み立てようとします。例えばArticlesController内にexposeの呼び出しが次のように2つあるとします。

/app/controllers/articles_controller.rb

expose(:article)
expose(:articles) { Article.order(:name).where(:visible => true) }

単数形のarticleメソッドの呼び出しは、複数形のarticlesスコープに基づいて個別の記事を取得しようとするので、上のコードでは、個別の記事を探すときにそれがvisibleな場合のみ記事が返されます。この振る舞いを止めたければ、複数形の方を、より内容を具体的に示す名称に変更する必要があります。今回はvisible_articlesに名称を変更します。

/app/controllers/articles_controller.rb

expose(:article)
expose(:visible_articles) { Article.order(:name).where(:visible => true) }

これで、2番目のexposeの呼び出しは、前の単数形のアクションの元となるデフォルトのスコープとは見なされません。このような修正を行った場合は、当然ビューからそのメソッドを呼び出している箇所も修正する必要があります。

デフォルトの振る舞いを変更する

exposeメソッドのデフォルトの振る舞いを変更する必要がある場合は、default_exposureを呼び出してブロックを渡します。そのブロックで定義する振る舞いが、デフォルトの振る舞いよりも優先されます。exposeに渡される名前はdefault_exposureのブロックに渡されます。

class MyController < ApplicationController
  default_exposure do |name|
    ObjectCache.load(name.to_s)
  end
end

通常はデフォルト設定をこのように変更する必要はありませんが、必要なときにはそうすることもできることを覚えておいてください。

今回のエピソードはこれで終わりです。decent_exposureはコントローラを整理する優れた方法であり、自分の処理フローに合致すると思ったらぜひ利用を検討してみてください。