今回は Observer パターンについて書いていきたいと思います。
参考にしたのは「Rubyによるデザインパターン」の書籍です。もう販売されていなくて中古でしか買えないですが、Ruby プログラマーがデザインパターンを学ぶのにすごいいい本だと思います。
どうしてもデザインパターンは Java や C++ で説明されているものが多く、これらの言語を触ったことがないと読むのが中々しんどかったです。
何となくは分かるんですが詳細が分からず、この本にたどり着きました。
Observer パターンとは
Observerパターンは、主に Subject(発信者)と Observer(消費者)からなる二つのコンポーネントで構成されます。
Subject は、状態の変化が観察されるオブジェクトであり、Observer はその変化に反応するオブジェクトです。
Subject は、一つ以上の Observer を持つことができ、Subject の状態に変更があるたびに、登録された全ての Observer に通知します。
このパターンの機能は、Observer が Subject に対して「登録」することから始まります。
Subject の状態に変更が発生すると、Subject は自身に登録されている全ての Observer に通知し、それぞれの Observer は更新を行うための特定のアクションを実行します。
このメカニズムにより、Subject と Observer 間の疎結合が保たれ、オブジェクト間の相互作用が効率的かつ動的に行われます。
Observer パターンの利点は、オブジェクト間の通信がインターフェースによって明確に定義され、オブジェクト同士が互いに独立しているため、Subject や Observer の一部を変更しても他の部分に影響を与えにくいことです。
実際の例を見たほうが分かりやすいので、例を見ていきます。
Observer パターンを使わない例
今、Employee クラスと Payroll クラス(給与部門)があるとします。Payroll は給料の変更の通知を行います。
class Employee
attr_reader :name
attr_accessor :title, :salary
def initialize(name, title, salary)
@name = name
@title = title
@salary = salary
end
end
class Payroll
def update(employee)
puts "Cut a new check for #{employee.name}!"
puts "The new salary is #{employee.salary}!"
end
end
給料が変わったときに通知を行うように変更します。
class Employee
attr_reader :name
attr_accessor :title, :salary
def initialize(name, title, salary, payroll)
@name = name
@title = title
@salary = salary
@payroll = payroll
end
def salary=(new_salary)
@salary = new_salary
@payroll.update(self)
end
end
payroll = Payroll.new
fred = Employee.new('Fred', 'Crane Operator', 30000, payroll)
fred.salary = 40000 # => Cut a new check for Fred! The new salary is 40000!
このコードの問題点は給与の変更通知を Employee クラスにハードコーディングしている点です。
例えば、税務署員も給与の変更について知りたいと言ってきた場合どうなるでしょう。
以下のように Employee クラスに変更を加えないといけません。つまり、Emploee が誰が給与の変更に関して知りたがっているか知っている状態です。
class Employee
attr_reader :name
attr_accessor :title, :salary
def initialize(name, title, salary, payroll, tax_man)
@name = name
@title = title
@salary = salary
@payroll = payroll
@tax_man = tax_man
end
def salary=(new_salary)
@salary = new_salary
@payroll.update(self)
@tax_man.update(self)
end
end
他にも通知を必要とするオブジェクトが増えるたびに Employee クラスを変更しなければなりません。
Observer パターンを使う例
Employee は具体的に誰が通知を知りたがっているかは必要なく、必要なのは変更に関心のあるオブジェクトの一覧です。
@observers にオブジェクトの一覧を持つようにして、変更時にはそれらの update メソッドを呼び出すようにします。
それらオブジェクトは update メソッドを必ず持たなければいけません。
class Employee
attr_reader :name
attr_accessor :title, :salary
def initialize(name, title, salary)
@name = name
@title = title
@salary = salary
@observers = []
end
def salary=(new_salary)
@salary = new_salary
notify_observers
end
def notify_observers
@observers.each do |observer|
observer.update(self)
end
end
def add_observer(observer)
@observers << observer
end
def delete_observer(observer)
@observers.delete(observer)
end
end
fred = Employee.new('Fred', 'Crane Operator', 30000)
fred.add_observer(Payroll.new)
fred.salary = 40000 # => Cut a new check for Fred! The new salary is 40000!
これにより、税務署員も給与の変更について知りたいと言ってきた場合でも、Employee クラスを変更する必要はありません。
class TaxMan
def update(chnaged_employee)
puts ''#{changed_employee.name}に新しい税金の請求書を送ります"
end
end
fred = Employee.new('Fred', 'Crane Operator', 30000)
fred.add_observer(Payroll.new)
fred.add_observer(TaxMan.new)
fred.salary = 40000 # => Cut a new check for Fred! The new salary is 40000! Fredに新しい税金の請求書を送ります
オブザーバに対する責務を分離する
オブザーバーに関する責務を持ったコードは分離することができます。こうすることで、他のクラスでオブジェクトを観測する必要がある度に同じコードを書くことをなくせます。
このときに継承とモジュールの2つの方法がありますが、この場合はモジュールの方が適しているのでモジュールにします。
module Subject
def initialize
@observers = []
end
def add_observer(observer)
@observers << observer
end
def delete_observer(observer)
@observers.delete(observer)
end
def notify_observers
@observers.each do |observer|
observer.update(self)
end
end
end
class Employee
include Subject
attr_reader :name, :address, :salary
def initialize(name, title, salary)
super() # Subject の initialize を呼び出す
@name = name
@title = title
@salary = salary
end
def salary=(new_salary)
@salary = new_salary
notify_observers
end
end
fred = Employee.new('Fred', 'Crane Operator', 30000)
fred.add_observer(Payroll.new)
fred.salary = 40000
Employee クラスの初期化では Subject の initialize を呼び出すために super() を書いているところに注意してください。
また、Ruby では標準で Observable モジュールが備わっているので、これを使うことができます。
require 'observer'
class Employee
include Observable
attr_reader :name, :title, :salary
def initialize(name, title, salary)
super()
@name = name
@title = title
@salary = salary
end
def salary=(new_salary)
@salary = new_salary
changed
notify_observers
end
end
fred = Employee.new('Fred', 'Crane Operator', 30000)
fred.add_observer(Payroll.new)
fred.salary = 40000
コードブロックとしてのオブザーバー
コードブロックを使うことでより簡単にオブザーバーに登録することができます。
以下のように Subject をコードブロックように書き換えることで、Observer はクラスでなくてもよくなります。
module Subject
def initialize
@observers = []
end
def add_observer(&observer)
@observers << observer
end
def delete_observer(observer)
@observers.delete(observer)
end
def notify_observers
@observers.each do |observer|
observer.call(self)
end
end
end
class Employee
include Subject
attr_reader :name, :address, :salary
def initialize(name, title, salary)
super()
@name = name
@title = title
@salary = salary
end
def salary=(new_salary)
@salary = new_salary
notify_observers
end
end
fred = Employee.new('Fred', 'Crane Operator', 30000)
fred.add_observer do |changed_employee|
puts "Cut a new check for #{changed_employee.name}!"
puts "The new salary is #{changed_employee.salary}!"
end
fred.salary = 40000
改めて Observer パターンとは
Gof は「何らかのオブジェクトが変化した」というニュースの発信者(Subject)と消費者(Observer)の間にインターフェースを作ることを Observer パターンと呼んでいます。
上記の例では、Employee が Subject で Payroll が Observer です。Observer がサブジェクトの状態の変化を知ることに関心がある場合(もっとも関心があるから Observer という名前ですが)、その Observer は Subject に登録されます。
Rails プロジェクトでの Observer パターン
Rails 4 までの時代には ActiveRecord::Observer が存在していましたが、現在は削除されています。
理由はモデルのライフサイクルイベントに追加され暗黙的にメソッドが呼ばれることになるので、テストがしにくかったりバグの原因になりやすいからということでした。
今でも Gem として提供はされてますが、もうメンテナンスされていないので避けたほうがよさそうです。
余談ですが、コールバックも同じ理由で嫌う開発者が多いですが、いつかなくなる日がくるんでしょうか。
さいごに
Rails のプロジェクトでモデルのコールバックを使って履歴を保存しているのは見たことあり、このパターンだったのかと勉強になりました。
知っているだけでコードの見方が変わるのでデザインパターンを知っておくことはいいコードを書く上でとても大切だと感じています。
ここまで読んでいただきありがとうございました!