読者です 読者をやめる 読者になる 読者になる

MogLog

メモというか日記というか備忘録というか

『Rubyによるデザインパターン』学習ノート:Observer

Observerパターンとは、何らかのオブジェクトが変化したというニュースの発信者と受信者の間に綺麗なインタフェースを作るパターンのこと。
ニュースを持っているクラスをサブジェクトクラスと呼び、ニュースの受信者をオブザーバーと呼ぶ。

システムを構築する各部分が全体の状態に注意を払わなくてはならないような、高度に統合されたシステムの構築に関する問題。例えば、誰かの給料が変わった時に経理部門に知らせる必要のあるような人事システムを考えたい。

注目すべきは、発信源としての振る舞いがあること。
たとえば、Fredが昇給したら、彼のEmployeeオブジェクトは世界中に向けて「すみません、私に何かが起こりました!」と周囲に知らせる。
すると、Employeeオブジェクトと関係のあるオブジェクトがすべきことはただひとつ。「事前にEmployeeオブジェクトから通知がもらえるように登録されること」だけ。これにより、それらのオブジェクトはタイムリーに変更の情報を受け取る事ができるようになる。

# -*- encoding: utf-8 -*-
# 愚直に給料の変更を経理部門に伝えるためのサンプルコード
class Payroll
  def update(changed_employee)
    puts "#{changed_employee.name} no tameni kogitte wo kirimasu"
    puts "his saraly is #{changed_employee.salary} now"
  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
fred = Employee.new('Fred', 'Crane Operator', 30000, payroll)
fred.salary = 35000

上述のコードは、経理部門への給与の変更通知をハードコーディングしている。
もしも、他のオブジェクトに対して、Fredの財政状態を通知する必要が出てきた場合、どうすればよいのだろうか。
変更の通知を受け取りたい部分は「変化しやすい事柄」であり、変化しない部分と分離しなくてはいけない。

class Payroll
  def update(changed_employee)
    puts "#{changed_employee.name} no tameni kogitte wo kirimasu"
    puts "his saraly is #{changed_employee.salary} now"
  end
end

class TaxMan
  def update(changed_employee)
    puts "Send new tex to #{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
    #@payroll.update(self)
    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)
    @observer.delete(observer)
  end
end

fred = Employee.new('Fred', 'Crane Operator', 30000.0)
payroll = Payroll.new
fred.add_observer(payroll)
taxman = TaxMan.new
fred.add_observer(taxman)

fred.salary = 300000

rubyにおけるObseverパターンで必要になるのは、上述の例とほとんど変わらない。必要になるものは以下。
・オブザーバを保持するための配列
・配列を管理するための2つのメソッド(要素の追加、削除)
・変更が発生したときの通知用メソッド
オブザーバを監視するためのメソッドを分離して、継承するパターンを考えたい。

class 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 < Subject
     # .....
end

継承による問題点は、その時点のスーパークラス以外のクラスを基底クラスにする可能性を断ち切ってしまうこと
仮に、ドメインモデル上、すでに継承済みのクラスがあった場合、詰んでしまう。
この問題の解決方法がモジュール。
モジュールとは「クラス間で共有可能なメソッドと定数のパッケージ」。1つしか割り当てられないスーパークラスの場を奪うことはない。

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()
    @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.0)
payroll = Payroll.new
fred.add_observer(payroll)
taxman = TaxMan.new
fred.add_observer(taxman)

fred.salary = 300000

※ super() → 読み込んだモジュールのinitializeを呼び出す効果を持つ