追記:
個人的な最終結論が出たのでそちらをまとめました。
概要
タイトルは1年ちょっと前の自分に向けた自戒なので絶対に使うのが悪というわけではない(と思う)。
だがぼくにはうまい付き合い方が見つけられそうになかったのでやめろという強い口調を意図して使っている。
ActiveSupport::Callback is 何?
これ→activesupport/lib/active_support/callbacks.rb
Railsガイドにも記載されているのでRailsチュートリアル完走するかそれ相当のことをしていたら使う使わないは別にして知っているだろうと思う。
# サンプルでよくみるやつ
class User < ApplicationRecord
after_create :send_confirm_to_user
private
def send_confirm_to_user
# sign upしたら最終確認のために送られるメールだと思ってほしい
UserMailer.signup_confirmation(to: mail).deliver
end
end
当時の考え
当時コールバックで実装すべきか、Service Objectで実装すべきか一瞬考えた……がrailsというフレームワークが提供している機能なので巨人の肩には乗っておいたほうが後々良いだろうと判断した。
その結果、いま地獄をみている。
ActiveSupport::Callbackの問題
このエントリを書こうとしてから何故か見つかるという非常に悲しい思いを今まさにしているが以下のような現象に苦しまされるのでコールバックは使わない、使うとしてもコントロール可能な実装を用いて最小限で使うべきというのがいまの感想だ。
- Rails: メールをActive Recordのコールバックで送信しないこと(翻訳)
- Rails: Active Recordのコールバックを避けて「Domain Event」を使おう(翻訳)
- メールを送信する処理をどこに書くべきか?
地獄とはなんであるか?
他にもあるが大きくは「テストがしにくい」「影響範囲がわからない」の2点にほぼ集約される。
-
FactoryBot.create :user
したらコールバックが走ってメールが送信される -
rails console
でUser.create
したらメールが送信される - migration内で
User.create
したらメールが(ry
などなど。
特に注意しないといけないのがconsole
とmigration
内でモデルを使うときでこれらのときにコールバックが実行されることを意識していることはまれだと思う。
ぼくは全く意識しておらず実行して「😱😱😱」と何度かなった、マジでやばい。
上記の「Rails: メールをActive Recordのコールバックで送信しないこと(翻訳) おたより発掘」でも言及されているが「コールバックは副作用である」という説明が非常に端的かつわかりやすいと思う。
ぼくの場合FactoryBotで非常に困ったというかテストが書きにくくなってしまいました。
これはコールバックそれ自体だけではコントロールが難しい点に問題がある。
A、B、Cというユーザを新規作成し、それぞれに別のOrganization
に所属させるテストを書きたいだけなのにコールバックでメールを送信してしまうとこのときメールが実行されてしまいます。
一応コールバックには skip_callback
で処理をスキップさせることができます……が今回の例のような非常に関心の高い User
というモデルはテストでもよく使われますがそのたびに毎回 skip_callback
を書く?
正気の沙汰じゃないですね!!!
またコールバックはデータの意図が明示的でないので問題が起こったときに調査しにくくなってしまいました。
極端な例ですが
-
Organization
を削除したときにMembers
をafter_destroy
で削除 -
Members
のafter_destroy
でUser
のorganization_id
をnil
に変更
という処理を書いていた場合 Organization
が Member
と User
に影響することを常に意識しなければいけない。
だが、現実的にこれは非常にしんどい。
テストを書いているときもコンソールを叩いているときもマイグレーションを実行するときにもどこでなにをするにも Organization
が削除されたときにこれらを意識してデータが意図しない状態になっていないかを検知できなくてはいけないことになります。
将来的に影響する範囲が縮小するのであればいいですが、現実はその反対でより依存する数が増えるだろうと思います。
ぼくは増えてしまい大変困りました、困っています。
ではどうするか?
1番、というか実質ほぼこれだと思うのですが、Service Object相当な場所に実装を移します。
# CreateUserAndSendMailService.new(user).create をコントローラなどで呼び出す
class CreateUserAndSendMailService
def initialize(user)
@user = user
end
def create
raise "なんかエラー起こった" unless @user.save
UserMailer.signup_confirmation(to: @user.mail).deliver
end
end
どうしてもコールバックでないといけない事情がある場合はやむなく以下のようにして通常はコールバックは実行されないようにし、どうしても必要な箇所だけで can_send_mail=true
にすることでコールバックを実行する許可を与える実装にするかなと思います。
ただ基本的には上記のService Objectで実装するほうがよいと考えています。
理由としては依存を特定のService Objectに押し込めることができるため、リファクタリングをすることになっても無用な不安(自分の検知していないところに影響しないか)などを心配する必要がなくなります。
最悪なくならずとも最小限に抑えることができます。
class User < ApplicationRecord
# 初期値は nil になる
# https://ref.xaio.jp/ruby/classes/module/attr_accessor
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
# sign upしたら最終確認のために送られるメールだと思ってほしい
UserMailer.signup_confirmation(to: mail).deliver
end
end
user.can_send_mail = true
user.save # メールが送信される
other.new(name: 'alice', mail: 'alice@example.com')
other.save # メールは送信されない
コールバックとは関係ないのですがどこでなにが呼ばれるかわからない不安はコントローラでActiveRecord
のwhere
やsave
などのメソッドが出ているから心配になるのであって最初からCreateUserService
やDestroyOrganizationService
のようなものがありコントローラはそれが呼び出されているだけであれば心配や不安に思うこともないのでコールバックだけの責任ではないですね。
とはいえ、コールバックがコードに現れたらそれはほぼ実質的に黒なのでコードレビューの段階で阻止すべき事柄なのだなと学習しました。
まとめ
正直なところ default_scope
はあまり使うべきでない(unscope
があるとはいえ)というのは知っていたのだけど callback
もそうだとは思っておらず楽をしようとして大きな負債を積み上げてしまった。
rails
便利機能が非常に豊富である一方、この機能をうかつに使うとあとで大きなコストを払うことになる……みたいな初心者がよく躓くケースをまとめてRailsガイドに載せておいてくれといいたい。
普通に考えたらフレームワークが提供している機能のほうが自前で実装するよりもいいと思うじゃん。
追記
類似エントリにサジェストされていたおかげで↓の記事を読ませてもらった、2012年09月04日の話なので少し古いがテストがしにくいという問題は一応解決できそう。
ただ根本的解決というよりは暫定対応っぽい解決方法に感じるのでやはり Service Object
相当な実装のほうがいいのかなという印象は変わらず。