プログラミング 美徳の不幸

Ruby, Rails, JavaScriptなどのプログラミングまとめ、解説、備忘録。

意外と難しいObserver

ObserverとはActiveRecordが提供する、モデルに対するイベント処理を可能にするRailsの機構です。もちろん同様のことはモデル中に
before_create :メソッド名
の形で記述すればいいわけですが、Observerを用意することで別ファイルに出き、見通しがよいこと。および、観察対象のモデルをObserver側で追加したりできて自由度が広がるなどの理由によって存在します。僕は前者の理由によって使います。

さて、このモデルのコールバックないしObserverですが、普段どういうときに使うべきか?
というのは、実はコールバックにするかしないかは、個人的にRailsで最も難しい部分じゃないかと思うのです。

よくある処理は、以前バイト先で書いていたコードを引用すると

class Competition < ActiveRecord::Base
  after_create :send_mail

  private
  def send_mail
    sender = self.owner
    UserMailer.send_confirmation(sender).deliver
  end

のようなコードでしょう。


ただ、ある条件のときはメールを送らなくてよい場合もあるでしょう。例えばこのCompetitionは作成者にメールが行くわけですが、このCompetitionを運営者がキャンペーン的に作ることだってあるはずです。その場合に自分のところにたくさんメールが来たんじゃ困ります。

どうやってコールバックを分岐させますか?

まず考えるのはActiveRecordオブジェクトを介さない更新。つまりController側でCallbackさせたくない処理はSQLで更新するということ。

それはいくらなんでも無茶なって気がしますが、destroyに似たdeleteというメソッドはActiveRecordを介しませんので、確かにbefore_destroy/after_destroyならdeleteを使えば抜けられます。

しかしその方法ではcreate/update/save系のコールバックの中は入ってしまうし、しかもコールバックするかしないかの2択で、条件分岐はできません。

で、このコールバックには引数は渡せないんですね。渡せるのは上の例だとself, 下の例だとcompetitionオブジェクト,つまりコールバックの対象となるオブジェクトの情報しか使えません。

class CompetitionObserver < ActiveRecord::Observer
  observe Competition
  def after_create(competition)
    send_mail(competition)
  end
  
  private
  def send_mail(competition)
    ...
  end
end

他にやり方があったら教えて欲しいんですが、僕はもうcompetitionオブジェクトの属性としてskip_callbackのようなフラグを持たせてしまおうと思いました。

class Competition < ActiveRecord::Base
  attr_accessor :skip_callback
  attr_accessible :skip_callback
end
class CompetitionObserver < ActiveRecord::Observer
  observe Competition
  def after_create(competition)
    send_mail(competition) unless competition.skip_callback
  end
  ...
end

これなら

Competition.create(skip_callback:true)

とすればメールを送ることはありません。ちなみにこの方法はテーブルのカラムを必要としないアトリビュートの設定方法なので、他にもいろいろ応用できて便利です。

しかし、これはあくまでもskip_callbackというフラグを渡す方法を、消去法的にモデルにぶち込んだという話で、本来ならこんなものが入るのは気持ち悪い。

ここで一番最初の質問をもう一度考えてみましょう。モデルのコールバックないしObserver、どういうときに使うべきか?

結論から言って、callback内の条件分岐をモデルオブジェクトの状態で変更するか、そもそも条件分岐を必要としない場合ならcallbackにしてよいでしょう。
つまり、前述のcompetition.skip_callback = trueは、skip_callbackという名前だから気持ち悪いのであって、
competition.type = :campaign
として、:campaignかどうかで処理を分けるのは自然です。


私の経験上、このことを意識せずになんでもかんでもcreateの処理に頻出するものをコールバックにしたりしているとコールバックがぐちゃぐちゃになってスパゲティ化します。こういう状態なら、まだ素人が書く肥大化させたControllerのほうがわかりやすいというぐらいです。