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サイクルが落ちることもさほどありません。ビューのロジックを排除すれば、油断なく見張り続ける必要もなくなるのでエネルギーの節約にもなります。
概要
元サイトの許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。