Railsのビューは頭悪そうなぐらいシンプルに保つべし(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Railsのビューは頭悪そうなぐらいシンプルに保つべし(翻訳)

Railsプロジェクトに長年携わっているうちに、最初は純真きわまりないビューだったのが、いつしかネストだらけの複雑怪奇なRubyコードと入り組んだHTMLを煮込んだようなものに変わり果てていました。こうなってしまうと、理解するのも大変なら改修するのも大変です。ビューの取るに足らないような部分をちょっぴり修正するだけでも、エンドツーエンドテストを辛抱強く書いてはバグがつぶされたかどうかを確認することになります。しかもこの種のテストはテストスイートの総実行時間に激しく影響するので、これ以上テストを増やしたくないと思うのが人情です。重要でも何でもないエッジケースならなおさらです。テストにはそうしたexpectationが全部詰まっているので、エンドツーエンドテストが増えれば増えるほどビューの改修が困難になります。

ビューの機能をどれだけ増やさずにいられるかが決め手です。ビューのロジックが複雑になればなるほど、テストも困難になります。複雑になれば、HTMLやらボタンのクリックやらCSSのセレクタなどなど、考えなければならないことがその分増えるからです。

この問題のシンプルな解決方法は、ロジックを別のクラスに移動することです。別クラスに切り出されたロジックならば単体テストも楽になります。単体テストは実行も早く、書くのも簡単です。

そこから抜粋したビューで考えてみましょう。少々込み入ったロジックも含まれています。

<div class="checkout">
  <% if @order.line_items.count > 0 %>
    <% if (@order.total - @order.paid) > 0 %>
      <div class="outstanding-amount"><%= number_to_currency(@order.total - @order.paid) %></div>
    <% end %>
    <div class="all-the-line-items"></div>

    <% if @order.cancelled_at.nil? && (@order.total - @order.paid) > 0 %>
      <%= link_to "Cancel your order", cancel_order_path(@order) %>
    <% end %>
  <% else %>
    Your order is empty!
  <% end %>
</div>

ここで何が行われているのかを読み取るには、考える必要があります。特にif条件を読み解くにはしばらく時間がかかるでしょう。

Railsプロジェクトの場合、ロジックの移動先の有力な候補はビューデコレータです。ロジックだけをここに移し、HTMLレンダリングから出来る限り切り離します。HTMLレンダリングのテストを書くのは、単純な戻り値テストを書くよりずっと面倒だからです。

1つのデコレータにすべてのロジックを詰め込まなければならないということはありません。1つのページだけを担当するデコレータを作成することも、ページの特定のセクションだけを担当するデコレータを作成するのも、ありです。1つのデコレータが、デコレータでない別のクラスのロジックを委譲しても構いません。デコレータについて詳しくは、Railsアンチパターン: Decoratorの肥大化に譲ります。

さて、上のビューのデコレータは以下のような感じになります。

class OrderDecorator < Draper::Decorator
  delegate_all

  def checkout_possible?
    line_items.count > 0
  end

  def can_be_cancelled?
    cancelled_at.nil? && !complete?
  end

  def complete?
    unpaid == 0
  end

  def unpaid
    total - paid
  end
end

ロジックをデコレータに切り出してみると、かなり読みやすいビューになりました。

<div class="checkout">
  <% if @order.checkout_possible? %>
    <% unless @order.complete? %>
      <div class="outstanding-amount">$ <%= number_to_currency(@order.unpaid) %></div>
    <% end %>
    <div class="all-the-line-items"></div>

    <% if @order.can_be_cancelled? %>
      <%= link_to "Cancel your order", cancel_order_path(@order) %>
    <% end %>
  <% else %>
    Your order is empty!
  <% end %>
</div>

このビューにはテストの必要なエッジケースがほとんどないことが完全に明らかです。誤りは肉眼で確認してもよいくらいです。複雑なエンドツーエンドテストを書く代わりにシンプルなテストを書けば済むので、時間も節約できます。テストスイートの実行も速くなるので、TDDサイクルが落ちることもさほどありません。ビューのロジックを排除すれば、油断なく見張り続ける必要もなくなるのでエネルギーの節約にもなります。

関連記事

Railsアンチパターン: Decoratorの肥大化(翻訳)

Rails: テストのリファクタリングでアプリ設計を改良する(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ