「Fat Controllerをコードで防ぐ方法はないか?」という記事で、まさにFat Controllerを防ぐ私案として、Intent-Responseパターンを提示した。分かりづらいと思うので、もう少し説明してみる。
Fat Controlerの実例
ぼくが思うFat Controlerは、こういうものだ。
def diff
journal = Journal::AggregatedJournal.containing_journal(@journal)
field = params[:field].parameterize.underscore.to_sym
unless valid_diff?
return render_404
end
unless journal.details[field].is_a?(Array)
return render_400 message: I18n.t(:error_journal_attribute_not_present, attribute: field)
end
from = journal.details[field][0]
to = journal.details[field][1]
@diff = Redmine::Helpers::Diff.new(to, from)
@journable = journal.journable
respond_to do |format|
format.html
format.js do
render partial: 'diff', locals: { diff: @diff }
end
end
end
OpenProjectというRails製のOSSに含まれているコードである。JournalsControllerのアクションだ。
それほどFatに見えないかもしれないが、ビジネスロジックもプレゼンテーションロジックも含まれていて、コントローラが責務を担いすぎているようにぼくには思える。
たとえば、@journable の扱いがおかしい。JS用のテンプレートでは @journable を参照しない。だから、下記のように、HTMLが要求されたときだけ設定されるべきだ。
respond_to do |format|
format.html do
@journable = journal.journable
render
end
format.js do
render partial: 'diff', locals: { diff: @diff }
end
end
このような混乱が生じるのは、コントローラがプレゼンテーションロジックまで担ってしまっているからだ。
どうしたら防げるだろうか。
Intent
まず、ビジネスロジックについて考えてみよう。
JournalsController#diff のビジネスロジックは「diffの取得」である。HTMLが要求されたときでもJSが要求されたときでもレスポンスに含まれるのはdiffだけだ。
こうしたビジネスロジックを「Intent」と呼ぶことにする。JournalsController#diff にアクセスするユーザの意図、すなわちIntentは「diffの取得」ということである。
Response
一方、プレゼンテーションロジックについてはどうか。
JournalsController#diff のレスポンスは、表現形式(HTML or JS)を無視すれば、3種類考えられる。ステータスコードが404、400、200の場合に対応するものだ。
3種類のレスポンスに必要なロジックはそれぞれ異なる。404の場合は固定のレスポンスボディを返すだろうし、200の場合は表現形式に応じたレスポンスボディを組み立てることになるだろう。
こうしたレスポンスは、そのまま「Response」と呼ぶことにする。Viewと呼ばないのは、たとえば「204 No Content」のようにレスポンスボディのない応答もありえるからだ。Responseの呼ぶのがふさわしいだろう。
Fat Controlerのリファクタリング
ここまででビジネスロジックをIntentに、プレゼンテーションロジックをResponseに、それぞれ任せる方針が見えた。個々のIntentやResponseはオブジェクトとして扱うのが自然だろう。
となると、JournalsController#diff は下記のようにリファクタリングするのがよさそうだ。
def diff
status_code, outcome = Journal::Intent::Diff.new.execute(self)
responses = {
404: Journal::Response::FieldNotFound,
400: Journal::Response::AttributeNotPresent,
200: Journal::Response::Diff
}
responses[status_code].new.build(self, status_code, outcome)
end
こうすれば、コントローラはIntentの実行と、実行結果に応じたResponseの組み立てを制御するだけでよくなる。outcomeはIntentの結果を意味し、200時にはdiffが、400時にはエラーメッセージが含まれる。
(IntentやResponseにコントローラ自身を渡しているのは、渡した先でparamsやrenderなどを呼び出すと想定したからだ。他に良い手があるかもしれない)
Intent-Responseパターンのメリット
Intent-Responseパターンが絶対の正解だとは言わない。が、コントローラの可読性の低さに起因するエンバグは減るだろう。それだけでも、ぼくにはメリットが大きい。
