Railsコールバック多すぎ問題と解決策まとめ

こんにちは。Enjoy IT Life管理人の@nishina555です。

Railsにはコールバックという便利な仕組みがありますが、多用しすぎるとコードの見通しが悪くなり、バグの温床になることがあります。

この記事では、コールバックの多用がなぜ問題になるのか、どのように解消すべきか、そしてコールバックが適している場面はどこかをまとめます。

コールバック多用のアンチパターン

よく見られるアンチパターンとして、インスタンス変数をセットするためにbefore_actionを多用するケースがあります。

一つ一つが独立していればまだマシですが、複数のbefore_actionが依存し合っている場合はロジックの把握が困難になります。機能追加の際にバグを混入させる原因にもなりますし、各アクションで何がインスタンス変数としてセットされるのかを読み解くことも難しくなります。

解決方法

事前準備系コールバック(before_action)の解消

インスタンス変数のセットにはbefore_actionを使わず、各アクション内で直接セットするのが基本方針です。

特に、メソッドに分割したくなるほどインスタンス変数のセットが複雑な場合は、コールバックを利用しないようにしましょう。

副作用系コールバック(after_create等)の解消

after_createのような副作用を伴うコールバックの解消には、大きく分けて2つのアプローチがあります。

アプローチ1: コールバックの処理を別クラスに分割する

コールバックに書いていた処理を、専用のクラスに切り出す方法です。

具体的には、runcallexecuteなどのコマンドを実行するメソッドが1つだけ公開されている**コマンドクラス(PORO: Plain Old Ruby Object)**を作成します。複数のモデルが関わるような複雑な操作が必要になった場合は、それぞれ適切なコマンドクラスに分割します。

コントローラーでコールバック対象のインスタンスを引数にサービスクラスのインスタンスを生成し、サービスクラス内でトリガーおよび後続処理を実行する形です。

アプローチ2: pub/subパターン(ドメインイベント)で実現する

コールバックを使うとモデル自体に副作用が埋め込まれてしまいますが、ドメインイベントを使えば副作用の内容(メールを送る、外部サービスを更新する等)をモデルが意識する必要がなくなり、疎結合な設計になります。

「Domain Event」をパブリッシュし、イベントに対して応答するハンドラをサブスクライブするという構成です。Railsでpub/subを実現するgemとしては以下のようなものがあります。

コールバックが適している場面

コールバックがすべて悪いわけではありません。以下のような場面ではコールバックの利用が適切です。

認証・認可

認証・認可はコントローラー全体で利用するため、before_actionでの実装が適しています。ただし、onlyexceptオプションを使い出したら「設計が間違っているかも」と疑いましょう。onlyexceptが必要な場合は、コールバックをやめるか、コントローラーの分割を検討します。

ネストしたルーティングの親リソースのセット

親となるリソースのセットはコントローラー全体で行うため、こちらもbefore_actionが適しています。認証・認可と同様にonlyexceptが必要になったら設計を見直すサインです。

「連鎖がない」「単独で完結する」アクション

漏れなく実行されることを期待するアクションもコールバック向きです。具体例としてはバリデーション、ログ出力、メールやチャットへの通知などがあります。

ただし、「テストの時は実行されたくない」「バッチの時は実行されたくない」のように発火条件があるのであれば、コールバックで定義するのは適切ではありません。

コールバックの実行を制御する方法

どうしてもコールバックを使う場合は、実行有無を制御するアトリビュートを用意し、デフォルトをfalseにすることでコールバックの誤爆を防げます。

class User < ApplicationRecord
  attr_accessor :can_send_mail
  after_create :send_confirm_to_user, if: :can_send_mail?

  private

  def can_send_mail?
    can_send_mail.present?
  end

  def send_confirm_to_user
    UserMailer.signup_confirmation(to: mail).deliver
  end
end

別の例として、条件メソッドを使ってコールバックの発火を制御するパターンもあります。

class Message < ApplicationRecord
  has_many :mentions
  belongs_to :creator, class_name: 'User'
  belongs_to :channel

  after_create :create_here_mention, if: :here?
  after_create :create_channel_mention, if: :channel?

  def create_here_mention
    HereMentionCreator.call(message: self)
  end

  def create_channel_mention
    ChannelMentionCreator.call(message: self)
  end
end

まとめ

  • before_actionでのインスタンス変数セットの多用は避け、各アクション内で直接セットする
  • after_create等の副作用系コールバックは、コマンドクラスへの切り出しやpub/subパターンで解消する
  • 認証・認可やバリデーションなど、コントローラー全体で漏れなく実行したい処理にはコールバックが適している
  • コールバックを使う場合は、実行制御用のアトリビュートを用意して誤爆を防ぐ

参考

タグ: Rails , リファクタリング