Help us understand the problem. What is going on with this article?

Rubyデザインパターン 3日目 : Observer

More than 3 years have passed since last update.

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)

スクリーンショット 2015-07-27 11.25.28.png

 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 :salarysaraly=メソッドを用いているのは、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としての機能を完全にパーツ化できています。

実行の流れ

  1. add_observerメソッドによりオブザーバを設定します。
  2. 更新したい項目に値をセットします
  3. changes_completeメソッドによりオブザーバに連絡を送ります
  4. 無事にCHANGED_EMPLOYEEオブザーバがcallメソッドにより実行されます

 まとめ

Observerパターンは、中心クラスの変更を監視する必要があるときに用いられます。
中心クラスの役目はデータを変更し、それをオブザーバに教えるだけです。
またコードブロックも引数にとる形にすれば、より柔軟性は高まりますね。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした