Railsのパターンとアンチパターン3: ビュー編(翻訳)
Ruby on Railsのパターンとアンチパターンシリーズの第3回目にようこそ。前回までの記事では「一般的なパターンとアンチパターン」「Railsのモデル関連のパターンとアンチパターン」をそれぞれ取り上げました。今回は、Railsのビュー関連のパターンとアンチパターンをいくつか紹介します。
Railsのビューは、問題なく高速に動作するかと思えば、さまざまな問題が発生することもあります。本記事は、自信を持ってビューを扱えるようになりたい方や、このテーマについて詳しく知りたい方におすすめです。それでは早速始めましょう。
おそらくご存知かと思いますが、Railsフレームワークでは「設定より規約(Converntion over Configuration)」が重視されます。RailsはMVC(Model-View-Controller)パターンが重要なので、当然ビューのコードにも「設定より規約」が当てはまります。マークアップ(ERBファイルやSlimファイル)、JavaScript、CSSファイルもこれに含まれます。一見すると、ビュー層はとてもわかりやすくて簡単だと思うかもしれませんが、最近のビュー層にはさまざまな技術が混在していることをどうかお忘れなく。
ビューでは、JavaScript、HTML、CSSを使います。この3つはコードの混乱やばらつきにつながる可能性があり、そうなると長期的にあまり意味のない実装になってしまいます。ちょうど今回は、Railsのビュー層でよくある問題とその解決法をいくつか見ていく予定です。
ビューの「重量挙げ」
この種のミスはそれほど頻繁には起きませんが、いったん発生すると足手まといになります。ビューの中についついドメインロジックやクエリを直接書いてしまう人をよく見かけますが、これではビュー層の内部で汗水たらして重量挙げしているようなものです。面白いのは、Railsが実際にはこのような書き方を禁止していないという点です。Railsにはこの種の「セフティーネット」が存在せず、ビュー層ではどんな無茶な書き方も可能になっています。
定義に従えば、MVCパターンのビュー層には表示のためのロジックを置くべきであり、ドメインロジックやデータのクエリをそこに混ぜるべきではありません。RailsのERB(Embedded Ruby)ファイルを使ってRubyのコードを書くと、そのコードが評価されてHTMLになります。indexページで曲のリストを表示するWebサイトを例に取ると、ビューのロジックはapp/views/songs/index.html.erbファイルに置かれます。
ビューの「重量挙げ」の意味と、ビューでやってはならないことを説明するために、以下のコード例を見てみましょう。
<!-- app/views/songs/index.html.erb -->
<div class="songs">
<% Song.where(published: true).order(:title) do |song| %>
<section id="song_<%= song.id %>">
<span><%= song.title %></span>
<span><%= song.description %></span>
<a href="<%= song.download_url %>">Download</a>
</section>
<% end %>
</div>
上のコードにある巨大なアンチパターンは、songをマークアップの中でフェッチしていることです。データを取得する責務は、コントローラか、さもなければコントローラから呼び出されるサービスに委譲すべきです。コントローラで何らかのデータを準備しておいて、その後ビューで追加のデータをフェッチしている人をたまに見かけます。これは悪い設計であり、クエリの頻度を増やしてデータベースに負荷をかけるため、Webサイトが遅くなります。
代わりに、@songs
インスタンス変数をコントローラのアクションで公開し、マークアップでは以下のようにそのインスタンス変数を呼び出すべきです。
class SongsController < ApplicationController
...
def index
@songs = Song.all.where(published: true).order(:title)
end
...
end
<!-- app/views/songs/index.html.erb -->
<div class="songs">
<% @songs.each do |song| %>
<section id="song_<%= song.id %>">
<span><%= song.title %></span>
<span><%= song.description %></span>
<a href="<%= song.download_url %>">Download</a>
</section>
<% end %>
</div>
これらのコード例は完全からほど遠いことにご注意ください。コントローラのコードを読みやすくしてSQLパスタを避けたい方は、ぜひ前回の記事をご覧ください。また、ビュー層にロジックを置いてしまうと、他の開発者たちがそのロジックをあてにしてソリューションを構築してしまう可能性が高まります。
Railsにある機能を活用しよう
ここでは簡単な説明にとどめます。フレームワークとしてのRuby on Railsには、特にビューで重宝するヘルパーがどっさり用意されています。こうした気の利いたヘルパーを活用すれば、ビューを短期間で手軽に構築できます。Railsに慣れていないと、つい以下のようにERBファイルの中にHTMLを丸ごと手書きしたくなってしまうかもしれません。
<!-- app/views/songs/new.html.erb -->
<form action="/songs" method="post">
<div class="field">
<label for="song_title">Title</label>
<input type="text" name="song[title]" id="song_title">
</div>
<div class="field">
<label for="song_description">Description</label>
<textarea name="song[description]" id="song_description"></textarea>
</div>
<div class="field">
<label for="song_download_url">Download URL</label>
<textarea name="song[download_url]" id="song_download_url"></textarea>
</div>
<input type="submit" name="commit" value="Create Song">
</form>
このHTMLでも、以下のスクリーンショットのように「New Song」フォームをそれらしく表示できます。
しかしRailsでこんなプレーンHTMLを手書きする必要はありませんし、手書きするべきではありません。form_with
ビューヘルパーを使えば、Railsが代わりにフォームのHTMLを生成してくれます。form_with
はRails 5.1で導入され、それまでの古いform_tag
やform_for
(こちらに慣れている人もいるかもしれませんね)を置き換えるためのものです。それでは、form_with
を使うとコード量がどれだけ減るかを見てみましょう。
<%= form_with(model: song, local: true) do |form| %>
<div class="field">
<%= form.label :title %>
<%= form.text_field :title %>
</div>
<div class="field">
<%= form.label :description %>
<%= form.text_area :description %>
</div>
<div class="field">
<%= form.label :download_url do %>
Download URL
<% end %>
<%= form.text_area :download_url %>
</div>
<%= form.submit %>
<% end %>
form_with
は単にHTMLを生成するだけではなく、CSRF攻撃を防ぐための認証トークンも生成します。したがって、ほぼどんな場合でも指定のヘルパーを使う方が身のためです(指定のヘルパーはRailsフレームワークでスムーズに動きます)。仮にフォームをプレーンHTMLで送信しようとすると、リクエストに認証トークンが存在しないため失敗します。
Railsには、form_with
、label
、text_area
、submit
以外にも多数のビューヘルパーが用意されています。これらのビューヘルパーは、あなたが楽に生きられるために存在しているので、詳しく知っておくべきです。中でも光り輝いているのは、間違いなくlink_to
でしょう。
<%= link_to "Songs", songs_path %>
上のコードから以下のHTMLが生成されます。
<a href="/songs">Songs</a>
ヘルパーをひとつひとつ説明しているときりがありませんし、本記事のテーマからも外れますので、ヘルパーの詳細は割愛します。Webサイトで必要なヘルパーについてはRails Action Viewヘルパーガイドで探すことをおすすめします。
ビューコードの再利用と構成
ここに完全無欠なWebアプリケーションがあると想像してみましょう。ユースケースも完璧なのでif
-else
文は使われておらず、コントローラでフェッチしたデータをHTMLタグに押し込めるコードだけがあるとします。こんなアプリケーションはハッカソンや夢の中になら存在するかもしれませんが、現実のWebアプリケーションは、ビューをレンダリングするときに大量の分岐や条件判断を行います。
ページの一部を表示するロジックが複雑になりすぎてしまったら、どうするべきでしょうか?何か方法はあるのでしょうか?一般的な回答としては、モダンなJavaScriptライブラリかフレームワークで複雑なものを構築するということになるのでしょうね。しかし本記事のテーマはRailsのビューなので、ビューでどんな方法が使えるかを見てみることにしましょう。
カスタムヘルパーを後付けする
たとえば、ある曲の下にコールトゥアクション(CTA)ボタンを表示したいとします。しかしここでひとつ問題があります。ある曲にダウンロード用のURLが存在する場合と、何らかの理由でダウンロード用のURLが存在しない場合のどちらの可能性もあります。そういうときに、つい以下のようなコードを書きたくなってしまうかもしれません。
<!-- app/views/songs/show.html.erb -->
...
<div class="song-cta">
<% if @song.download_url %>
<%= link_to "Download", download_url %>
<% else %>
<%= link_to "Subscribe to artists updates",
artist_updates_path(@song.artist) %>
<% end %>
</div>
...
上のコード例を独立したプレゼンテーションロジックと思えば、それほど悪くなさそうですよね?しかし今はよくても、条件付きレンダリングが増えるとコードが読みづらくなってしまいます。そして条件が増えれば増えるほど、どこかでレンダリングがおかしくなる可能性も高くなります。
対抗手段のひとつは、この部分を別のヘルパーに切り出すことです。ありがたいことに、Railsではカスタムヘルパーを手軽に書ける方法も用意されています。app/helpersディレクトリの下に以下のようなSongsHelper
モジュールを書けます。
module SongsHelper
def song_cta_link
content_tag(:div, class: 'song-cta') do
if @song.download_url
link_to "Download", @song.download_url
else
link_to "Subscribe to artists updates",
artist_updates_path(@song.artist)
end
end
end
end
これで、ある曲のshowページを開くと引き続き同じ結果を得られます。しかしこのコード例はもう少し改善できます。コード例では@song
インスタンス変数が使われていますが、@song
がnil
になる場所ではこのヘルパーが使えなくなる可能性があります。そこで、インスタンス変数という外部依存をなくすために、以下のようにヘルパーに引数を渡す方法が使えます。
module SongsHelper
def song_cta_link(song)
content_tag(:div, class: 'song-cta') do
if song.download_url
link_to "Download", song.download_url
else
link_to "Subscribe to artists updates",
artist_updates_path(song.artist)
end
end
end
end
続いて、ビューで以下のようにヘルパーを呼び出します。
<!-- app/views/songs/show.html.erb -->
...
<%= song_cta_link(@song) %>
...
これで先ほどと同じ結果がビューで表示されます。カスタムヘルパーを利用するメリットは、今後ヘルパーでリグレッションが発生しないようにするためのテストを書ける点です。デメリットは、カスタムヘルパーがグローバルに定義されてしまうことで、ヘルパー名がアプリ全体で重複しないよう注意する必要があります。
Railsでカスタムヘルパーを書くのがあまり好きでなければ、draper gemでいつでもView Modelパターンを導入できます。あるいはView Modelパターンを手作りで導入しても構いません(それほど複雑にはなりません)。Webアプリを作り始めたばかりであれば、今のうちにひと手間かけてカスタムヘルパーを書いておき、もしカスタムヘルパーがつらくなるようだったら別のソリューションを使うことをおすすめします。
ビューをDRYにする
私がRailsを使い始めた頃に心底惚れ込んだのは、信じられないほど簡単にマークアップをDRYにできることでした。Railsにはパーシャル(再利用可能なコード)を作成してどこでもインクルードできる機能があります。たとえば、songsを複数の場所でレンダリングしていて、同じコードが複数のファイルにある場合は、songパーシャルを作成するとよいでしょう。
たとえば以下のような曲のshowページがあるとします。
<!-- app/views/songs/show.html.erb -->
<p id="notice"><%= notice %></p>
<p>
<strong>Title:</strong>
<%= @song.title %>
</p>
<p>
<strong>Description:</strong>
<%= @song.description %>
</p>
<%= song_cta_link %>
<%= link_to 'Edit', edit_song_path(@song) %> |
<%= link_to 'Back', songs_path %>
しかし上を同じマークアップで別のページにも表示したいとします。その場合は、app/views/songs/_song.html.erb
のようにファイル名冒頭をアンダースコアにした新しいファイルを作成します。
<!-- app/views/songs/_song.html.erb -->
<p>
<strong>Title:</strong>
<%= @song.title %>
</p>
<p>
<strong>Description:</strong>
<%= @song.description %>
</p>
<%= song_cta_link(@song) %>
以後、songパーシャルをインクルードしたい箇所で以下のように書くだけで利用できるようになります。
...
<%= render "song" %>
...
Railsは_song
パーシャルが存在するかどうかを自動探索して、存在する場合は表示します。カスタムヘルパーの例と同様、パーシャル内では以下のように@song
インスタンス変数を取り除いておくのがベストです。
# app/views/songs/_song.html.erb
<p>
<strong>Title:</strong>
<%= song.title %>
</p>
<p>
<strong>Description:</strong>
<%= song.description %>
</p>
<%= song_cta_link(song) %>
続いてsong
変数をパーシャルに渡し、再利用性を高めて他の場所でも利用できるようにしておく必要があります。
...
<%= render "song", song: @song %>
...
最後に
今回の記事は以上です。今回はRailsビューで踏む可能性のあるパターンとアンチパターンをいくつか解説しました。要点を以下にまとめます。
- UIでは複雑なロジックを避けること(ビューに重量挙げさせないこと)
- Railsのビューヘルパーにどんな機能があるかを知っておくこと
- コードをカスタムヘルパーやパーシャルで構造化し、再利用すること
- インスタンス変数に頼りすぎないこと
次回は、Railsのコントローラにおけるパターンと、気をつけないとかなり痛い目に合うアンチパターンをいくつか取り上げます。どうぞご期待ください。
それでは次回またお会いしましょう!
原文追記
Rubyのマジックに関する記事が公開されたらすぐ読みたい方は、元記事末尾のフォームにて「Ruby Magic」ニュースレターをご購読いただければ、新着記事を見逃さずに読めるようになります。
概要
原著者の許諾を得て翻訳・公開いたします。