ネットの海の片隅で

技術ネタの放流、あるいは不法投棄。

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_*ApplicationControllerAdmin::BaseController のような場所に定義されることになる。

おわりに

Rails で標準的に使われているけど、違和感を覚えている before_action :set_* について書いてみた。

なんやかんや言いつつも Rails はべんりなので、いい感じに折り合いをつけて付き合っていきたい。

追記

表参道.rb #38 で喋ってきた。