Observerについて
15 Dec 2023一般的にActiveModelでなにかモデルの状態を変更した場合はafter_save
や最近追加されたafter_save_commit
などのコールバックを使う。
デザインパターンの世界ではObserverというパターンが存在するようだ。
まずはChatGPTに質問してみた:
Observer パターンは、ソフトウェアデザインパターンの一つで、オブジェクトの状態の変化を他のオブジェクトに通知するためのものです。このパターンでは、あるオブジェクト(被観察者またはサブジェクトと呼ばれることもあります)がその状態が変化したときに、それに興味を持っている他のオブジェクト(観察者)に変更を通知します。これにより、被観察者と観察者の間で強い結合を避け、柔軟性を高めることができます。
Observer パターンの主な要素は次の通りです:
- 被観察者(Subject): 状態の変化を通知する対象となるオブジェクト。被観察者は観察者を管理し、状態が変化した際にそれらに通知します。
- 観察者(Observer): 被観察者の状態変化に興味を持っているオブジェクト。これらのオブジェクトは被観察者に登録され、状態の変更があると通知を受けることができます。
Observer パターンの基本的な動作は以下のようになります:
- 被観察者には観察者を登録するためのメソッド(通常は
addObserver
)が用意されています。- 観察者は被観察者に対して自分を登録します。
- 被観察者の状態が変化すると、登録された観察者全員に通知が行われます。通知メソッド(通常は
notifyObservers
)を呼び出すことによって実現されます。- 観察者は通知を受け取り、適切な処理を行います。
Observer パターンは、イベント処理、ユーザーインターフェイス、モデル-ビュー-コントローラ(MVC)アーキテクチャなどでよく使用されます。被観察者と観察者の分離により、システムの変更が簡単に行え、柔軟性が向上します。
例えばあるFooというモデルがあり、Foo#bar
を実行したあとにBazMailerがメールを送信するとしよう、こういった場合FooはBazMailerに依存する必要はないし、後からQuxNotifierというクラスを追加してもよいだろう。
ただ私もよく理解できなかったのだけれどもObserverはなんとなく関心の分離という意味のクラスではあるものの、コールバックとの違いがよく理解できなかった。
そこで続けてChatGPTに質問してみた:
ObserverとCallbackは、ソフトウェア開発において異なる概念を指しますが、共通して特定のイベントや状態の変化に対する反応を定義する手段です。
Observer パターン:
- 定義: Observer パターンは、オブジェクトの状態の変化を監視し、変化があった際に他のオブジェクトに通知するためのデザインパターンです。
- 構成要素: 被観察者(Subject)と観察者(Observer)があります。被観察者は状態の変化を通知し、それに興味を持っている観察者がその通知を受け取り、適切な処理を行います。
- 利点: オブジェクト間の疎結合を提供し、変更が容易で柔軟な設計を可能にします。例えば、GUIフレームワークのイベントハンドリングやMVC(Model-View-Controller)アーキテクチャでよく使用されます。
Callback:
- 定義: Callbackは、ある関数やメソッドを引数として渡し、別の関数やメソッドの実行中に特定のポイントで呼び出すことを指します。これは、非同期処理、イベント処理、またはコールバック関数として知られています。
- 構成要素: 通常は関数またはメソッドです。関数が別の関数を引数として受け取り、後者の関数が実行される際に、前者の関数が呼び出されます。
- 利点: コールバックは非同期処理やイベント駆動の場面で有用で、特定のイベント発生時に実行されるカスタムロジックを提供します。JavaScriptやRubyなどの言語でよく使用されます。
違い:
- パターン vs. 機能:
- Observerはデザインパターンで、オブジェクト間の状態の変化を通知するためのものです。
- Callbackは特定のイベントや条件が発生したときに、指定されたコード(関数やメソッド)を実行する手段です。
- 構成要素:
- Observerは被観察者と観察者という明示的な構成要素があります。
- Callbackは通常、関数やメソッドとして具現化されます。
- 使用シナリオ:
- Observerはオブジェクト間の状態変化に関連して使用され、疎結合な設計を促進します。
- Callbackは非同期処理やイベント駆動プログラミングでよく使用され、特定のイベントが発生したときに対応する処理を提供します。
簡単に言えば、Observerはオブジェクト間の関係性を扱うデザインパターンであり、Callbackはある処理が完了した際に他の処理を実行する手段を指します。
この回答を得た時点ではまだ納得行かなかったのだけれども、ここで重要なのは状態が変化するということにつきる。
イメージとしてはReactのsetState
が実行されたときに再描画が行われるようなイメージに近いと思う。
ChatGPTは親切なのでデザインパターンといえばJavaということで、Javaのコード例を提示してくれているのだがここでは割愛する。 Rubyでも同様にJavaをなぞるような簡易的なクラスの実装はできなくもないのだが、ObserverというまさにこのObserverパターンの実装を行うためのモジュールが存在する。
class Ticker
def run
if price != last_price
changed # notify observers
last_price = price
notify_observers(Time.now, price)
end
end
end
コードを抜粋してみたがここではchanged
が重要みたいだ。
Observerのコード自体はこの投稿の時点でも230行程度で、その大半がRDocのものなので実装を読み解くことはそこまで難しくなさそうである。
続けてnotify_observers
のコードも掲載してみよう:
def notify_observers(*arg)
if defined? @observer_state and @observer_state
if defined? @observer_peers
@observer_peers.each do |k, v|
k.__send__(v, *arg)
end
end
@observer_state = false
end
end
ちょうどdefined? @observer_state and @observer_state
がchanged?
にあたる部分だと思う。
つまり状態が変更されなければSubjectに対して通知は行われないということだ。
これが明確にコールバックとは異なる部分だろう。
ちなみにchanged?
というメソッドはActiveModelにも存在する。
ということは、Observer
をインクルードしてobserverを登録しておけばよさそうだ。
Rubyの
Observable
モジュールはObserverパターンを実装するためのもので、オブジェクトの状態変化を監視し、変化があった際に登録されたObserverに通知することができます。しかし、Observable
モジュールはRuby 2.5.0から非推奨とされ、推奨される代替手段は提供されていません。代わりに、通常のオブジェクト指向の原則を使ってObserverパターンを実装することが一般的です。
本当はこのあたりまでChatGPTにも踏み込んでほしかったのだけれども、なぜかChatGPTの世界線ではObservableは非推奨になっている。
class Foo < ApplicationRecord
include Observable
after_save -> { notify_observers(self) }
def bar
self.update(value: rand(100))
end
end
class Baz
def initialize(foo)
foo.add_observer(self)
end
def update(foo) # メソッドはupdateである必要がある
puts "value is #{foo.value}"
end
end
foo = Foo.create
Baz.new(foo)
foo.bar
理想はこのようなコードを書きたかった。
このコードであれば単にafter_save -> { Baz.new.update(self) }
でもよいのだが、あくまでFooはBazというクラスの存在に依存しないのがポイントである。
ただしObservableをインクルードしてしまうともともとRails側で定義されるはずのchanged?
がまるごと上書きされてしまう。
またnotify_observers
がchanged?
ではなくdefined? @observer_state and @observer_state
であるのでやはりActiveRecordの恩恵をうけるには自分でクラスを用意するほうがよさそうである。
NOTE: これを見越してChatGPTが非推奨と言ったのかは定かではない。
module NotifyObservable
extend ActiveSupport::Concern
included do
after_save_commit :notify_observers
end
def add_observer(observer)
unless observer.respond_to?(:update)
raise NoMethodError, "observer does not respond to `update'"
end
observers << observer
end
private
def observers
@observers ||= []
end
def notify_observers
observers.each { _1.update(self) } if observable?
end
def observable?
observers.present? && saved_changes?
end
end
なかなかよい感じである。 本当はObservableが使いたかったのだけれども、既に定義されているモジュールを上書きするわけにもいかない。
changed?
はafter_save
の時点でfalse
を返すのでタイミング的には少々異なる。
DBに更新がcommitされたタイミングが好ましいのでsaved_changes?
にした。
この変更に伴いFooは単純にNotifyObservableをインクルードするだけでよくなった。
class Foo < ApplicationRecord
include NotifyObservable
end
では実際に動かしてみよう:
$ rails c
Loading development environment (Rails 7.0.8)
=> #<Foo:0x00007f4841d43c18 id: 15, value: nil, created_at: Fri, 15 Dec 2023 02:13:22.984689000 JST +09:00, updated_at: Fri, 15 Dec 2023 02:13:22.984689000 JST +09:00>
>> foo.bar
=> true
>> Baz.new(foo)
=> #<Baz:0x00007f5c23c4d958>
>> foo.bar
value is 42
=> true
ここでのポイントはFoo#bar
を2度実行しているが、あくまでObserverの有無に関係ないところだ。
あとはBaz
をどのタイミングで呼び出すかであるけれども、ActiveJob関連のようにすでにコールバックが用意されているものであればこのパターンを使う恩恵はない。
つまりServiceクラスであったり、自分で定義した単純なクラスであればこの恩恵を手にすることができそうである。