Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Ruby on Railsのビューで押さえるべきパターンとアンチパターン

Ruby on Railsのパターンとアンチパターンシリーズ第3回へようこそ。これまでの記事では、一般的なパターン・アンチパターンやRailsモデルに関連するものを取り上げてきました。今回は、Railsのビュー(View)に関連するパターンとアンチパターンについて詳しく見ていきます。

Railsのビューは、時に完璧かつ高速に動作することもあれば、さまざまな問題を抱えることもあります。ビューの扱い方への自信を高めたい方や、このトピックについてさらに学びたい方にとって、本記事はきっと役立つはずです。それでは早速始めましょう。

ビュー層の特徴

ご存じの通り、Railsフレームワークは「設定より規約(CoC)」という思想に基づいています。そして、RailsはModel-View-Controller(MVC)パターンを重視しているため、この理念は当然ビューコードにも適用されます。対象となるのは、マークアップ(ERBやSlimファイル)、JavaScript、CSSファイルなどです。一見すると、ビュー層はシンプルで扱いやすいように思えますが、昨今のビュー層には複数の技術が混在していることを忘れてはいけません。

ビューではJavaScript、HTML、CSSを使用します。この3つが絡み合うことで、コードが混乱・非効率になり、長期的に見て意味のない実装につながることがあります。幸いにも、今回はRailsビュー層におけるよくある問題とその解決策を見ていきます。

ビューの「重量挙げ」問題

これは頻繁には起こらないミスですが、発生すると非常に目障りなものです。ドメインロジックやデータ取得処理を直接ビューの中に記述してしまうケースがあります。これは、ビュー層に重い処理(重量挙げ)を担わせていることになります。興味深いことに、Railsは実際にこれを簡単に行えてしまいます。セーフティネットがないため、ビュー層内では何でも自由にできてしまうのです。

MVCパターンの定義上、ビュー層にはプレゼンテーションロジックのみを含めるべきです。ドメインロジックやデータクエリを担当させるべきではありません。RailsではERB(Embedded Ruby)ファイルを使い、Rubyコードを記述してHTMLとして評価できます。例えば、インデックスページで曲の一覧を表示する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>

ここでの大きなアンチパターンは、マークアップ内で直接曲データを取得している点です。データ取得の責務は、コントローラーやコントローラーから呼び出されるサービスオブジェクトに委譲すべきです。コントローラーである程度データを準備しておきながら、ビューでさらに追加のデータを取得するコードを目にすることもあります。これは悪い設計であり、データベースへのクエリ発行頻度が増えてサイトの速度低下を招きます。

代わりにすべきことは、コントローラーのアクションで@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で新しい曲を作成するフォームは表示されますが、Railsではそのような素のHTMLを書く必要はありませんし、書くべきでもありません。form_withというビューヘルパーを使えば、HTMLを自動生成してくれます。form_withはRails 5.1で導入され、馴染みのある方もいるform_tagform_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攻撃を防ぐための認証トークン(authenticity token)も自動的に生成します。したがって、ほぼすべての場面で、Railsフレームワークとうまく連携する専用ヘルパーを使う方が得策です。素のHTMLフォームを送信しようとすると、有効な認証トークンがリクエストに含まれていないため失敗してしまいます。

form_withlabeltext_areasubmitなどのヘルパーのほかにも、Railsには標準で多数のビューヘルパーが付属しています。これらは開発者の負担を軽減するために存在するので、しっかり把握しておきましょう。「オールスター」のひとつと言えば、間違いなくlink_toです。

<%= link_to "Songs", songs_path %>

これは次のようなHTMLを生成します。

<a href="/songs">Songs</a>

各ヘルパーの詳細まで踏み込むと記事が長くなりすぎるため、今日は深入りしません。Rails公式のAction Viewヘルパーガイドに目を通し、自分のサイトに必要なものを選ぶことをおすすめします。

ビューコードの再利用と整理

理想的なWebアプリケーションを想像してみてください。理想的なユースケースではif文は存在せず、コントローラーから受け取ったデータをHTMLタグの間に出力する純粋なコードだけがあります。そんなアプリケーションが存在するのは、ハッカソンか夢の中くらいのもので、現実世界のアプリケーションには、ビューをレンダリングする際に大量の分岐や条件が含まれます。

ページの一部を表示するロジックが複雑になりすぎたとき、どうすればよいでしょうか?一般的な答えとしては、モダンなJavaScriptライブラリやフレームワークを採用して複雑なものを構築するという選択肢もあります。しかし、本記事はRailsビューがテーマなので、ビュー内部で使える選択肢を見ていきましょう。

カスタムヘルパーの活用

例えば、曲の下にCTA(行動喚起)ボタンを表示したいとします。ただし、ここに落とし穴があります。Songはダウンロード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

曲の詳細ページを開いても、以前と同じ結果が得られます。しかしこの例はさらに改善できます。上の例ではインスタンス変数@songを使用していますが、@songnilになる場所でこのヘルパーを使おうとすると、利用できません。そこで、インスタンス変数という外部依存を断ち切るために、引数を渡せるようにしましょう。

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によるViewModelパターンを採用することもできます。あるいは、自分でViewModelパターンを実装することも可能です。Webアプリを作り始めたばかりなら、まずはカスタムヘルパーからゆっくり始め、それが苦痛になったら別のソリューションに切り替えることをおすすめします。

ビューをDRYに保つ

Railsを使い始めた頃に私が本当に気に入っていたのは、マークアップを驚くほど簡単にDRY(Don't Repeat Yourself)化できる点です。Railsにはパーシャル(partial)——再利用可能なコード断片——を作成し、任意の場所に組み込める機能があります。例えば、複数箇所で曲を表示しており、同じコードが複数のファイルに重複しているなら、曲用のパーシャルを作成するのが理にかなっています。

例えば、現在次のように曲を表示しているとします。

# 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) %>

あとは、曲パーシャルを組み込みたい場所で次のように書くだけです。

...
 
<%= 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コントローラーのパターンとアンチパターンを取り上げます。お楽しみに。

それでは、また次回!

  1. 【Rails入門】scopeの使い方を徹底解説!基本構文から引数付きスコープ、クラスメソッドとの違いまで

    Railsにおける「スコープ(scope)」とは何か?そしてなぜ便利なのでしょうか? 答えはこうです。 スコープとは、scopeメソッドを使ってモデル内に定義するカスタムクエリのことです。よく使う検索条件に名前をつけて再利用できるようにする仕組みと言えます。 すべてのスコープは2つの引数を受け取ります。 名前:コード内でこのスコープを呼び出す際に使用します。 ラムダ(lambda):実際のクエリ処理を実装します。 具体的には次のように書きます。 class Fruit < ApplicationRecord scope :with_juice, -> { where(jui

  2. Ruby on Railsとは?初心者にもわかる仕組み・魅力・学び方を徹底解説

    Ruby on Railsとは? Ruby on Rails(略称:RoR)は、世界で最も人気のあるオープンソースのWebアプリケーションフレームワークです。プログラミング言語「Ruby」をベースに構築されており、シンプルなサイトから大規模で複雑なサービスまで、幅広いWebアプリケーションの開発を支援します。 そもそもフレームワークとは? フレームワークとは、ソフトウェア開発の際に土台となる構造を提供してくれるコードやツール、ユーティリティの集合体です。あらかじめ用意された構造に沿ってコードを書くことで、プログラムが整理され、保守性も高まります。正しく使いこなせるようになれば、開発作業は格段に