LoginSignup
42
32

More than 5 years have passed since last update.

Rubyによるデザインパターン【Observer】-本日のニュースをお届けします-

Last updated at Posted at 2014-06-15

概要

Rubyによるデザインパターン第5章。
Observer Pattern。

Rubyによるデザインパターン5原則に則って理解する。

どんなパターンか

あるオブジェクトの状態に関心のあるオブジェクトに、都度通知を送る。

ニュースの発信源(Subject)とニュースの消費者(Observer)間に綺麗なインターフェイスを用意する。

Subject

あるニュースを配信するクラス

Observer

あるニュースを得ることに関心があるクラス

メリット

ニュースの発信者と受信者の間の依存関係を排除する。

問題のあるコード

従業員の給与の変化を経理部門に伝えるシステム

従業員クラス

class Employee
  attr_reader :salary
  attr_accessor :title, :name

  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

経理部門クラス

class Payroll
  def update(changed_employee)  # Subjectオブジェクトを受け取る
    puts "#{changed_employee.name}の給料が#{changed_employee.salary}ドルに上がりました!"
  end
end

クラスの利用

payroll = Payroll.new
john = Employee.new('john', 'worker', 100, payroll)
john.salary = 200
=> johnの給料が200ドルに上がりました!

問題

もし経理部門以外にも通知したくなったら?

→今はEmployeeクラスに手を入れる必要がある。
本質的にはEmployeeに対する変更など何もないにも関わらず・・。

そこで、

変化する事項(「従業員の給与の変更」というニュースを誰が受け取るか)を、Employeeオブジェクトから分離する。

→必要なのは、Employeeオブジェクトの変化に関心のあるオブジェクトの一覧。

従業員クラス

class Employee
  attr_reader :salary
  attr_accessor :title, :name

  def initialize(name, title, salary)
    @name = name
    @title = title
    @salary = salary
    @observers = []
  end

  def add_observer(observer)
    @observers << observer
  end

  def salary=(new_salary)
    @salary = new_salary
    notify_observers
  end

  private
  def notify_observers
    @observers.each do |observer|
      observer.update(self)
    end
  end

end

経理部門クラス

class Payroll
  def update(changed_employee)  # Subjectオブジェクトを受け取る
    puts "#{changed_employee.name}の給料が#{changed_employee.salary}ドルに上がりました!"
    puts "経理部門は#{changed_employee.name}に小切手を切ります!"
  end
end

税務署員クラス

class Taxman
  def update(changed_employee)
    puts "#{changed_employee.name}の給料が#{changed_employee.salary}ドルに上がりました!"    
    puts "税務署員は#{changed_employee.name}に新しい税金請求書を送ります!"
  end
end

クラスの利用

[4] pry(main)> john = Employee.new('john', 'worker', 100)
[5] pry(main)> john.add_observer(Payroll.new)
[6] pry(main)> john.add_observer(Taxman.new)
[7] pry(main)> john.salary = 200
johnの給料が200ドルに上がりました!
経理部門はjohnに小切手を切ります!
johnの給料が200ドルに上がりました!
税務署員はjohnに新しい税金請求書を送ります!

なかなか良くなった。
しかし、他のクラスにもサブジェクトの機能を持たせたくなった時、都度同じコードを書く必要がある。

モジュール化(1)

サブジェクトとしての機能をモジュール化し、ポータブルにする。

Subjectモジュール

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

モジュール利用(クラスのSubject化)

class Employee
  include Subject

  attr_reader :salary
  attr_accessor :name, :address

  def initialize(name, title, salary)
    super()  # Subject Moduleのinitializeを行う
    @name = name
    @title = title
    @salary = salary
  end

  def salary=(new_salary)
    old_salary = @salary
    @salary = new_salary
    if old_salary != new_salary  # oldとnewが異なっている場合のみ通知
      notify_observers
    end
  end

end

Employeeのinitializeメソッドの中でsuper()を呼び出すことに注意。
ここでSubjectモジュールのinitializeを呼び出している。

モジュール化(2)

実はRubyには、observableというobserver用Moduleが存在する。

モジュール利用(クラスのSubject化)

require 'observer'

class Employee
  include Observable

  attr_reader :salary
  attr_accessor :name, :address

  def initialize(name, title, salary)
    @name = name
    @title = title
    @salary = salary
  end

  def salary=(new_salary)
    old_salary = @salary
    @salary = new_salary
    if old_salary != new_salary  # oldとnewが異なっている場合のみ通知
      changed  # changedメソッドの呼び出し
      notify_observers
    end
  end

end

changedメソッドとは

Observableには、オブザーバへ冗長な通知を避けるために、
オブジェクトが本当に変更されたかどうかを示すBooleanフラグが存在する。
changedは、これをtrueに設定する。

notify_observersを呼び出すごとに、変更フラグはfalseへとリセットされる。

ブロックによるオブザーバー

ブロックによるオブザーバーによって簡潔なコードを実現する。
また、Observable(モジュール(2))はブロックをサポートしていないため、
独自実装であるモジュール(1)を拡張して実現してみる。

Subjectモジュール(ブロックをサポート)

module Subject

  def initialize
    @observers=[]
  end

  def add_observer(&observer)  # blockを受け取り、procへ変換(&)してobserverとして登録
    @observers << observer
  end

  def delete_observer(observer)
    @observers.delete(observer)
  end

  def notify_observers
    @observers.each do |observer|
      observer.call(self)  # observer(Proc)を呼び出す
    end
  end

end

モジュール利用(クラスのSubject化)

class Employee
  include Subject
  attr_accessor :salary, :name, :address

  def initialize(name, title, salary)
    super()
    @name = name
    @title = title
    @salary = salary
  end

  def salary=(new_salary)
    old_salary = @salary
    @salary = new_salary
    if old_salary != new_salary  # oldとnewが異なっている場合のみ通知
      notify_observers
    end
  end
end

呼び出し

[13] pry(main)> john = Employee.new('john', 'worker', 100)
[14] pry(main)> john.add_observer do |changed_employee|  # observerブロックの登録
[14] pry(main)*   puts("#{changed_employee.name}に新しい小切手を切ります!")
[14] pry(main)* end
[15] pry(main)> john.salary = 200
johnに新しい小切手を切ります!

まとめ

  • 変わるもの(オブザーバー)と変わらないもの(サブジェクト)を分離して、変化に強い構造へ
  • サブジェクトの機能自体はモジュール化してボータブル化
  • ブロックによるオブザーバーの登録も可能

Strategyパターンとの比較

形は似ている

Observerパターン:サブジェクトがオブザーバーを呼び出す
Strategyパターン:コンテキストがストラテジを呼び出す

違いは目的

Observerパターン:サブジェクトで発生したイベントをオブザーバーへ通知する
Strategyパターン:コンテキストが、何か特定の処理を行うためにストラテジを呼び出す

以下へ続く

【Composite】 -世界は再帰的(部分は全体、全体は部分)-
http://qiita.com/kidachi_/items/6cb73b2bbc875e9bef6d

42
32
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
32