Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: ビューには可能な限りロジックを書かないこと(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。


  • 2018/05/07: 初版公開
  • 2023/03/10: 更新

Rails: ビューには可能な限りロジックを書かないこと(翻訳)

Railsプロジェクトに長年携わっているうちに、最初は単純そのものだったビューが、いつしかネストだらけの複雑怪奇なRubyコードと入り組んだHTMLを煮込んだようなものに変わり果ててしまいます。こうなってしまうと、理解するのも改修するのも大変です。

ビューのほんの小さな部分を修正するだけでも、エンドツーエンドテストを辛抱強く書いてはバグがつぶされたかどうかを確認することになります。しかもこの種のテストはテストスイートの総実行時間に激しく影響するので、テストを増やしたくないのが人情です。特に、重要でないエッジケースのテストならなおさらです。エンドツーエンドテストではあらゆる期待が盛り込まれるので、増やせば増やすほどビューの改修が困難になります。

ビューには可能な限り機能を持たせないことが重要です。ビューのロジックが複雑になればなるほど、テストも困難になります。複雑になれば、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プロジェクトの場合、ロジックの移動先の有力な候補はビューのdecoratorです。ロジックだけをdecoratorに移して、可能な限りHTMLレンダリングから切り離します。HTMLレンダリングのテストを書くのは、単純な戻り値テストを書くよりずっと面倒だからです。

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

さて、上のビューの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

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

<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: テストのリファクタリングでアプリ設計を改良する(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。