Rubyデザインパターン学習のために、自分なりに読書の結果をまとめていくことに決めました。第3日目はObserverです。(http://www.amazon.co.jp/gp/product/4894712857/ref=as_li_qf_sp_asin_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=4894712857&linkCode=as2&tag=morizyun00-22)
3日目 Observer
今日のパターンは、Observerパターンです。
このパターンは変化に追従し、観察することです。
例を挙げると、スプレッドシートのようなソフトウェアの開発時、一つのパラメータを変更したときにそのパラメータを変更した事が他の部分に知れ渡ることが必要なケースが多い時に、このパターンは特に有効になってきます。
これから、より具体的な例で考えてみます。
従業員データを管理するコードがあり、その中のデータを一つでも変更すると、通知が送られるようなシステムです。
class Employee #単に従業員のデータを保持しているだけのクラス
attr_reader :name
attr_accessor :title, :salary
def initialize(name, title, salary)
@name = name
@title = title
@salary = salary
end
end
momozono = Employee.new("Momozono Tarou", "Normal", 200000) #データを入力
momozono.salary = 220000 #昇給
上記のコードは、ただ単に従業員データを保持するコードです。
平社員で20万円の給料をもらっているMomozono Tarouさんがいるということですね。
そして、salaryパラメータにアクセサを付けて代入することにより、給料を22万に昇級をさせています。
このコードだけでは出力の手段を持たないので、アウトプットの機能を付けてみましょう。
class Payroll
def update(changed_employee)
puts "#{changed_employee.name}さんについての情報です。"
puts "彼の給料は現在#{changed_employee.salary}です"
end
end
class Employee
attr_reader :name, :title
attr_reader :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
momozono = Employee.new("Momozono Tarou", "Normal", 220000, payroll)
momozono.salary = 210000
Employeeオブジェクトに出力用のPayrollオブジェクトを渡しています。
これにより@payroll
にPayrollオブジェクトが入ります。salaryメソッドが実行されたときに@salary
の値が更新され、それと同時にupdateメソッドが呼び出され、給与が更新されたことを通知するという設計です。
attr_accessor :salary
を用いずにattr_reader :salary
とsaraly=
メソッドを用いているのは、Payrollオブジェクトに値を渡すためです。
これで、最低限の機能である、給与の更新とその通知機能が実装されました。
さて、この状態からもう一つ機能を増やして、「給料が更新されたときに、税金の請求書を送る機能」を追加してみましょう。(細かいところはつっこまないでください汗)
Observerパターンを構築してみる
class Payroll
def update(changed_employee)
puts "#{changed_employee.name}さんの情報です"
puts "彼の給料は今#{changed_employee.salary}円です"
end
end
class Taxman
def update(changed_employee)
puts "#{changed_employee.name}に新しい税金の請求書を送ります!"
end
end
class Employee
attr_reader :name, :title
attr_reader :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
end
momozono = Employee.new('Momozono', 'Normal',220000)
payroll = Payroll.new
taxman = Taxman.new
momozono.add_observer(payroll)
momozono.add_observer(taxman)
momozono.salary = 230000
@observers
というパラメータを作ることにより、そこにオブザーバを配列として格納します。配列の形で機能を持つ事により、機能の拡張がとても簡単になります。
salary
メソッドからnotify_observers
メソッドを呼び出すことによって通知を制御しています。
機能を配列の形で持つというニュアンスが既にステキですよね。可能性を感じます。
機能を分割する!
さて、このままでも従業員データのセットとObserverの機能は保たれていますが、従業員のデータセットのためのEmployeeクラスの中に、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.update(self)
end
end
end
class Employee
include Subject
attr_reader :name, :address
attr_reader :salary
def initialize(name, title, salary)
super() #moduleのinitialize呼び出し
@name = name
@title = title
@salary = salary
end
def salary=(new_salary)
@salary = new_salary
notify_observers
end
end
こうすることで、Observer提供機能は全てモジュールに分離することに成功しました!
classではなくmoduleにすることにより、継承ベースの連携を避けています。
Rubyらしい構成に...
module Subject
def initialize
@observers = []
end
def add_observer(&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_accessor :name, :title, :salary
def initialize(name, title, salary)
super()
@name = name
@title = title
@salary = salary
end
def title=(new_title)
old_title = @title
if old_title != new_title
@title = new_title
end
end
def salary=(new_salary)
old_salary = @salary
if old_salary != new_salary
@salary = new_salary
end
end
def changes_complete
notify_observers
end
end
CHANGED_EMPLOYEE = lambda do |changed_employee|
puts "従業員の名前が変更されました: #{changed_employee.name}さん"
puts "給料: #{changed_employee.salary}"
puts "職業: #{changed_employee.title}になりました"
end
momozono = Employee.new("Momozono", "Normal", 220000)
momozono.add_observer(&CHANGED_EMPLOYEE)
momozono.salary = 0
momozono.title = "無職"
momozono.changes_complete
上記のコードでは、給与や等級が変更されたときのみ、通知が飛ぶようになっています。
lambdaを使う事により、Observerとしての機能を完全にパーツ化できています。
実行の流れ
-
add_observer
メソッドによりオブザーバを設定します。 - 更新したい項目に値をセットします
-
changes_complete
メソッドによりオブザーバに連絡を送ります - 無事に
CHANGED_EMPLOYEE
オブザーバがcall
メソッドにより実行されます
まとめ
Observerパターンは、中心クラスの変更を監視する必要があるときに用いられます。
中心クラスの役目はデータを変更し、それをオブザーバに教えるだけです。
またコードブロックも引数にとる形にすれば、より柔軟性は高まりますね。