Observer パターン for Ruby / Rails

この記事は本の感想、備忘録です。

各項目のサンプルコードは本書に掲載されているものではなく、復習のために自分で書いたものです。

Observer パターン

プログラム内のオブジェクトのイベントを他のオブジェクトへ通知する処理で使われるデザインパターンの一種です。(by wikipedia

ざっくりしたシーケンス図です。 f:id:meikotan:20210223101759j:plain

パターン適用前の構造がオブジェクトのイベントと他のオブジェクトへ通知する処理が密結合だった場合、他のオブジェクトへ通知する処理に変更が発生するたびにイベント発行オブジェクトを書き換える必要があります。Observer パターンでは通知する処理に共通インターフェースを適用することで、イベント発行オブジェクトはそのインターフェースに依存するのみで、具体的な通知処理への依存をなくすことができます。

例えば、社員の給料計算が終わった後に結果を社員とその上司にメール通知する場合を考えます。Observer パターンを使わない場合、給与計算するイベントはメール通知処理の具体的なことまで把握しています。一方で Observer パターンを適用した場合、給与計算イベントはメール通知処理のことを把握しません。把握しているのは給与計算イベントの後に何らかの処理をするということだけです。

# Observer パターンを使わない場合
def calculate_salary
  # 給与計算処理

  # 計算後にする処理
  NotificationMailer.exe(user) # ユーザにメール送信
  NotificationMailer.exe(manager) # 管理者にメール送信
end

# Observer パターンを使う場合
def calculate_salary
  # 給与計算処理

  # 計算後にする処理
  @observers.each do |observer|
    observer.update(self) # 具体的な処理内容を知らない
  end
end

「観察」より「通知」になっている

Javaデザインパターン本には

observer という言葉の本来の意味は「観察者」ですが、実際には Observer 役は能動的に「観察」するのではなく、Subject 役から「通知」されるのを受動的に待っていることになります。Observer パターンは、Publish-Subscribe パターンと呼ばれることもあります。publish(発行)と subscribe(購読)と言う表現の方が、実際にあっているかもしれませんね。

と書いてあるのですが、Observer パターンと Pub/Sub パターンは異なるパターンと説明されている記事もあります。

Observer パターンとは異なる Pub/Sub パターン

ここでの Pub/Sub パターンと Observer パターンでの大きな違いは、通知に必要な「通知先を把握する役割」と「通知する役割」をどのクラスが持つか?ということです。Observer パターンではそれを Subject が持っていましたが、Pub/Sub パターンはそれを別のクラス(Channel クラス)に切り出します。結果、Publisher と Subscriber の間に通知を管理する Channel が挟まり、Publisher と Subscriber が直接やり取りすることはなくなります。

Observer vs Pub-Sub pattern
Observer vs Pub-Sub pattern | Hacker Noon

参考)PythonにおけるPub/Subパターンの実装

ActionCable もこの形ですね。

Ruby / Rails

標準ライブラリに Observable がある

module Observable (Ruby 3.0.0 リファレンスマニュアル)

require 'observer'

class LifeGame
  include Observable
  
  def next_generation
    # xxx 何かしらの処理

    changed # 通知する・しないのフラグ更新が必要。引数はデフォルト true
    notify_observers(self) # これを呼ぶとフラグは初期化(false)される
  end
end

Observer の処理をコードブロックで渡しても OK

class LifeGame
  def initialize
    @observers = []
  end

  def add_observer(&observer) # ブロックを受け取れるようにする
    @observers << observer
  end

  def next_generation
    # xxx 何かしらの処理
    notify_observer
  end

  private

  def notify_observer
    @observers.each do |observer|
      observer.call(self) # proc.call
    end
  end
end

# main
life_game = LifeGame.new
life_game.add_observer do |changed_field|
  puts("次の世代になりました。")
  # 次の世代のセルを表示する処理
end

ActiveRecord::observer

ActiveRecord::Observer

モデルの callback 処理を Observer クラスに切り出すことができます。Observer クラスはファイル名からモデルのマッピングを行うため(COC)、クラス名を変えない場合はマッピングを明示する必要はありません。

class UserObserver < ActiveRecord::Observer
  def after_create(user)
    Notifications.create_user("Registration completed", user).deliver
  end
end

今度使いたいです٩( 'ω' )و