#259 Decent Exposure
- Download:
- source codeProject Files in Zip (97.3 KB)
- mp4Full Size H.264 Video (21.4 MB)
- m4vSmaller H.264 Video (12.8 MB)
- webmFull Size VP8 Video (29.3 MB)
- ogvFull Size Theora Video (28.9 MB)
今回のエピソードでは、decent_exposureというgemを紹介します。このgemはシンプルですが優れたコンセプトを持っています。これを使うことによって、インスタンス変数を使わずに、ビューからアクセスできるメソッドのインターフェイスをコントローラ内に作成できます。このgemは、exposeというメソッドを使ってこのインターフェイスを定義します。
decent_exposureを見る前に、手作業でこのコンセプトを実装してみましょう。対象とするのは簡単なブログアプリケーションで、複数のArticles
(記事)とそれに対する複数のComments
(コメント)からなります。
ArticlesController
には、標準的なコントーラのコードが含まれています。たとえばindex
アクションでは、@articles
というインスタンス変数を生成します。
class ArticlesController < ApplicationController def index @articles = Article.order(:name) end #Other actions omitted. end
index
ビューのコードでは、@articles
を用いて各記事を繰り返し読み込んで表示しています。
最初にRailsを使い始めた頃、コントローラのインスタンス変数へのアクセスがビューと共有されていることに違和感を覚えなかったでしょうか? 通常それらはクラス内のプライベート変数となっているべきではないかと。そこで今回は、ビューに対してメソッドを公開することでデータを共有する、もう一つのアプローチを見ていきます。これを実現するために、単独のモデルかモデルのリストを返すプライベートメソッドを、コントローラ内に作成します。
まずindex
アクションから、複数のarticleを取得するコード行を削除して、それをarticles
というメソッドに貼付けます。@articles
をキャッシュとして利用し、記事のリストが一度だけしか取得されないようにします。そのために||=
演算子を使用し、コントーラやビューの他の場所ではこのインスタンス変数を参照しないようにします。articles
をヘルパーメソッドにして、ビューで使えるようにします。
class ArticlesController < ApplicationController def index end private def articles @articles ||= Article.order(:name) end helper_method :articles end
ビューでは、インスタンス変数の呼び出しを新しく作ったarticles
メソッドに置き換えます。
<% 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
メソッドよりも少し複雑になります。というのも、渡されるパラメータによって違う動作をしなければいけないからです。メソッドはこのような形になります。
def article @article ||= params[:id] ?Article.find(params[:id]) : Article.new(params[:id]) end helper_method :article
もしid
パラメータが存在すれば、メソッドはそのid
のArticle
を探します。なければ、article
パラメータの内容に基づいて新しい記事を作成します。@article
インスタンス変数をこの新しく作ったメソッドに置き換えることによって、記事を検索したり新規作成する行をアクション内からなくすことができます。
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
ビューは修正後は次のようになります。
<% 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
メソッドを使って、前半で作ったarticles
とarticle
メソッドと似た方法でビューに対してモデルにアクセスする手段を提供できるようになります。expose
メソッドは、デフォルト設定で以下のように動作します。id
パラメータでモデルを探し、もしそれが見つからなければ、見つけられた適当なパラメータを用いて新しいモデルを作成します。つまり、single modelsの検索や作成にはデフォルト値を使うことができます。別の動作が必要であれば、メソッドにブロックを渡してその中に定義します。キャッシュの処理は、decent_exposureが自動でおこなってくれます。
ではこれをアプリケーションに組み込んでみましょう。まずGemfile
にgemを追加し、bundle
コマンドを実行します。
source 'http://rubygems.org' gem 'rails', '3.0.5' gem 'sqlite3' gem 'nifty-generators' gem 'decent_exposure'
これでArticlesController
内に書いたarticle
とarticles
の各メソッドを、2つのexpose
の呼び出しに置き換えることができます。
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はネストされたリソース(例えば今回のアプリケーションの場合の、記事の下にぶら下がったコメント)をどう取り扱うのでしょうか?
Blog::Application.routes.draw do root :to => "articles#index" resources :articles do resources :comments end end
CommentsController
は次のような形になります。
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
の呼び出しに置き換えます。個々のArticle
とComment
の取得にはデフォルトの動作をそのまま使えますが、コメントのリストを取得するには動作のカスタマイズが必要です。コントローラでインスタンス変数を設定しているコード行を削除し、残ったコードのうちのインスタンス変数の部分を対応するメソッドの呼び出しに置き換えます。これらの修正をおこなうとコントローラはきれいに整理されました。
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に生成されたメソッドを呼び出すように修正します。次に示すフォーム部品ファイルを参照してください。
<%= 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つあるとします。
expose(:article) expose(:articles) { Article.order(:name).where(:visible => true) }
単数形のarticleメソッドの呼び出しは、複数形のarticles
スコープに基づいて個別の記事を取得しようとするので、上のコードでは、個別の記事を探すときにそれがvisible
な場合のみ記事が返されます。この振る舞いを止めたければ、複数形の方を、より内容を具体的に示す名称に変更する必要があります。今回はvisible_articles
に名称を変更します。
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はコントローラを整理する優れた方法であり、自分の処理フローに合致すると思ったらぜひ利用を検討してみてください。