before_action、どう使う? Fizzyのコードを読んで整理してみた
はじめに
この記事は、Railsで開発をしていて before_action の使いどころに迷ったことがある方に向けて書いています。
最近Railsで開発をしていて、before_action の使い方についてコードレビューで指摘を受けました。
具体的には、リソース取得(@card = Card.find(params[:id]) のような処理)を before_action で書いていたところ、「アクションメソッドが増えて only オプションを使い始めると、どのアクションでどの変数が使えるか把握しづらくなる」という理由で、やめてほしいと言われました。
その通りだなと納得はしたものの、では before_action はどういうときに使うべきなのか、自分の中で明確な基準がないことに気づきました。
そこで使い方の参考を得るため、37signalsが公開しているOSS basecamp/fizzy のコードを読んでみました。
Fizzyは37signalsが公開しているKanban型のタスク管理ツールです。Railsの生みの親であるDHH氏が所属する会社だけあって、Railsらしいシンプルなコードが書かれています。そのソースコードは、明確で簡潔なRailsコードの書き方を学ぶ上で非常に参考になります。
この記事では、Fizzyのコードから学んだ before_action の使い方を紹介します。
パターン1:ApplicationController経由で全Controllerに適用
まず、ApplicationController を見てみます。
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Authentication
include Authorization
# ...
end
認証処理は Authentication というConcernにまとめられています。
# app/controllers/concerns/authentication.rb
module Authentication
extend ActiveSupport::Concern
included do
before_action :require_account
before_action :require_authentication
end
# ...
end
ポイント:
-
ApplicationControllerにinclude Authenticationされている - すべてのControllerは
ApplicationControllerを継承している - → 個別のControllerでは意識せずに、自動的に認証チェックが働く
# app/controllers/cards_controller.rb
class CardsController < ApplicationController
# ApplicationControllerを継承しているので、
# 自動的に require_authentication が実行される
end
共通の認証をbefore_actionで実装するのは、一番オーソドックスな使い方だと思います。自分もbefore_actionと聞いて最初に思い浮かぶのはこのパターンでした。
パターン2:個別のControllerで直接定義
次に、CardsController を見てみましょう。
# app/controllers/cards_controller.rb
class CardsController < ApplicationController
before_action :set_board, only: %i[ create ]
before_action :set_card, only: %i[ show edit update destroy ]
before_action :ensure_permission_to_administer_card, only: %i[ destroy ]
def show
end
def edit
end
def destroy
@card.destroy!
# ...
end
private
def set_board
@board = Current.user.boards.find params[:board_id]
end
def set_card
@card = Current.user.accessible_cards.find_by!(number: params[:id])
end
def ensure_permission_to_administer_card
head :forbidden unless Current.user.can_administer_card?(@card)
end
end
ポイント:
-
set_cardやset_boardは、そのController固有のbefore_action - 他のControllerでは使わないので、Concernにはせず直接定義
リソース取得
before_action :set_card, only: %i[ show edit update destroy ]
def set_card
@card = Current.user.accessible_cards.find_by!(number: params[:id])
end
複数のアクションで共通して必要なリソース取得をbefore_actionでまとめ、各アクションは本質的な処理に集中できるようにしています。
実は、これは冒頭で書いた「コードレビューで指摘されたパターン」そのものです。
指摘の通り、only オプションが増えると、どのアクションでどの変数が使えるか把握しづらくなる面はあると思います。それでもFizzyでは積極的に使われているのが印象的でした。
正解は一つではないのかもしれません。チームの方針やコードベースの規模によって、判断が分かれるところだと思います。
権限チェック
before_action :ensure_permission_to_administer_card, only: %i[ destroy ]
def ensure_permission_to_administer_card
head :forbidden unless Current.user.can_administer_card?(@card)
end
アクション固有の権限チェックもbefore_actionで行われています。権限がなければここで処理を中断するため、アクション本体には権限チェックのコードが入りません。
これもわかりやすい使い方ですね。「条件を満たさなければ処理を中断する」というガードの役割で、before_actionとして自然だなと思います。
まとめ:before_action の使い分け
| パターン | 例 | 適用範囲 | 定義場所 |
|---|---|---|---|
| Concern → ApplicationController | require_authentication |
全Controller | Concern |
| Controller直接定義 |
set_card, ensure_permission_to_*
|
そのControllerのみ | 各Controller |
Fizzyでは:
- 認証・認可(全員に必要)→ Concern + ApplicationController
- リソース取得・権限チェック(そのController固有)→ Controller直接定義
という使い分けがされています。
共通しているのは、「処理を続行してよいか?」の判断 または 「必要なリソースの準備」 という点です。
逆に、ビューの表示フラグ(@show_header = true など)や、特定のアクションでしか使わないデータの取得には使われていませんでした。
Fizzyのコードを読んで、before_action の使い方にもいろいろなスタイルがあることがわかりました。レビューで指摘されたパターンも、OSSでは普通に使われていて、結局はチームでの合意が大事なんだろうなと思います。
余談:Fizzyの設計思想
Fizzyのコードを読んでいて、コントローラーの実装が非常にシンプルなことに気づきました。
STYLE.md にも以下のように書かれています:
In general, we favor a vanilla Rails approach with thin controllers directly invoking a rich domain model.
このアプローチは「Vanilla Rails」と呼ばれているようです(参考:Vanilla Rails is plenty)。
その分、モデル層はこんな感じになっています
例えば Card モデル :
class Card < ApplicationRecord
include Assignable, Attachments, Broadcastable, Closeable, Colored, Entropic, Eventable,
Exportable, Golden, Mentions, Multistep, Pinnable, Postponable, Promptable,
Readable, Searchable, Stallable, Statuses, Storage::Tracked, Taggable, Triageable, Watchable
コントローラーをシンプルに保ち、ビジネスロジックをモデル(とconcern)に集約するおもしろ設計だなと思いました。
Discussion