Rails の before_action :set_* って必要?
表題の通り。
Rails でよくある
class HogesController < ApplicationController before_action :set_hoge, only: %i[show] def show end private def set_hoge @hoge = Hoge.find(params[:id]) end end
みたいなのっているの? という話。
set_*
がつらい理由
暗黙的に副作用があることが行なわれている
インスタンス変数の初期化と代入という重大な副作用がしれっと暗黙的に行なわれている。
暗黙的な順序依存性が発生しがち
はじめはシンプルに
before_action :set_hoge
とかしていても、機能追加などをしていくうちに
before_action :set_hoge before_action :set_fuga before_action :set_foo before_action :set_bar
みたいなことになりがち。
これらの set_*
が完全に独立していればまだ良いが、実際には関連するリソースを扱うことが多くて、「set_fuga
を呼ぶ前には set_hoge
を呼んでおかなければならない」みたいなことになりがち。
action の関心がわかりにくい
Rails の controller におけるインスタンス変数は view で使う変数、すなわちレスポンスとして送り返すために必要なものが入っている。
インスタンス変数が暗黙的に代入されると、その action がどういうことに関心があるのかがパッと見ではわからない。
代わりにどう書くか
シンプルな場合
そのまま書けば良い。
class HogesController < ApplicationController def show @hoge = Hoge.find(params[:id]) end end
これくらいの場合で set_*
するのは慣習以外の何物でもなく、メリットがない気がする。
上記ほどシンプルではない場合
具体的には、複雑だったり重要だったりする絞り込みを行なっていて、それを重複させたくない場合。
そういうケースでは set_*
な private method を書くんじゃなく、find_*
な private method を書けば良い。
先に find したオブジェクトへの依存がある場合は依存を明確にするために引数として渡す。
class HogesController < ApplicationController def show @hoge = find_hoge(params[:id]) @fuga = find_fuga(@hoge) end def edit @hoge = find_hoge(params[:id]) end def update @hoge = find_hoge(params[:id]) if @hoge.save else end end private # @param id [String] # @return [Hoge] def find_hoge(id) Hoge.very.complex.condition.find(id) end # @param hoge [Hoge] # @return [Fuga] def find_fuga(hoge) hoge.fugas.very.important.condition.first end end
「暗黙的な書き方 vs. 明示的な書き方」という好みの問題もあるかもしれないが、少なくとも自分にとってはこの方が何が行なわれているか明白だし action の関心も明らかにされていてわかりやすい。
before_action を使うべきとき
念のために書いておくと、すべての before_action
を滅ぼしたいわけではない。
使ったほうが便利なケースも存在していて、ざっくり2種類かなと思っている。
認証など
よくあるメソッド名でいうと authenticate!
とか verify_token
みたいなもの。
action が呼ばれるにあたって、満たしておくべき事前条件のようなものが存在し、その条件を満たさないときは action を実行しないような類のもの。
別の言い方をすると、before_action
の旧名である before_filter
という名前がしっくり来る感じのやつ。
横断的に使用される変数の初期化・代入
典型的には set_current_user
のようなもの。
そのアプリケーションにおいて、ある程度横断的に使われる変数の初期化・代入は set_*
でやってしまっても良いと思う。
理由としては、いちいち各 controller に書くのはさすがに面倒というのがひとつ。
もうひとつは、アプリケーションの全体もしくは論理的に分けられた一部分で横断的に使われるのであれば、そのアプリケーションを開発する人が知っていて然るべき知識としてしまって良いと思うから。
横断的に使用されるという性質上、この類の set_*
は ApplicationController
や Admin::BaseController
のような場所に定義されることになる。
おわりに
Rails で標準的に使われているけど、違和感を覚えている before_action :set_*
について書いてみた。
なんやかんや言いつつも Rails はべんりなので、いい感じに折り合いをつけて付き合っていきたい。
追記
表参道.rb #38 で喋ってきた。