Murayama blog.

AIの民主化。

Observer

Rubyによるデザインパターン生活3日目。in 市ヶ谷。
今日はObserverパターンを取り上げます。

Rubyによるデザインパターン

Rubyによるデザインパターン


Observerってのは「観測者」っていう意味が一番しっくりくるかな。
あるオブジェクトの状態変化を観測し、
変化が発生したことを知らせるのが目的です。


著者のラス・オルセンさんも言ってるけど、
Observerとなるクラスが観測者にあたるんだけど、
実際に観測者に変化を通知するのは、観測対象となるオブジェクト(Observable)なります。
#その辺でObserverパターンのネーミングは若干微妙な気もします。


そんな話はさておき、今回登場するクラスは以下のとおりです。

  • Employee
    • 従業員クラス。名前、役職、給料といった状態をもつ。状態が変化したら観測者(Observer)へ通知する。
  • Payroll
    • 給与明細クラス。従業員の給料の変化などを観測するObserverクラス。
  • TaxMan
    • 税金クラス。従業員の給料の変化などを観測するObserverクラス。


つづいてソースコードです。
まず、Employeeクラス。

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

インスタンス変数@observersがポイント。
ここに観測者(Observer)となるオブジェクトを保持します。
@observersへの追加にはadd_observerメソッドを使います。
状態であるsalaryが変化した場合、notify_observersメソッドが呼び出されるので、
結果として観測者クラス(Observer)のupdateメソッドが呼び出されます。


つづいて、観測者(Observer)となるPayrollクラスです。

class Payroll
  def update(changed_employee)
    puts "#{changed_employee.name}のために小切手を切ります。"
    puts "給料は#{changed_employee.salary}です。"
  end
end

変更通知用のupdateメソッドを持っています。
引数には観測対象であるEmployeeオブジェクトを受け取ります。


つづいてTaxManクラス。こちらもObserverクラスになります。

class TaxMan
  def update(changed_employee)
    puts "#{changed_employee.name}に新しい税金の請求書を送ります。"
  end
end

Payrollクラスと同じくupdateメソッドを実装しています。


実行用のプログラムです。

fred = Employee.new("Fred", "Crane Operator", 30000.0)

payroll = Payroll.new
fred.add_observer(payroll)
taxman = TaxMan.new
fred.add_observer(taxman)

fred.salary = 35000.0


そんで、実行結果です。

Fredのために小切手を切ります。
給料は35000.0です。
Fredに新しい税金の請求書を送ります。


以上が一般的なObserverパターンの流れです。
Payroll、TaxMan以外の観測者(Observer)クラスが追加された場合も、同様に処理できます。


ここまでのコードでまずいところは、
Employeeクラスの中に観測対象者(Observable)として状態や振る舞いが実装されてしまっているところ。
Employeeクラスが読みにくいものになっています。


なんで、コードを分離してあげます。
観測対象者(Observable)に該当するコードを抽出します。
今回はSubject(観測対象)という意味あいのクラスとして抽出します。


が、Subjectクラスをスーパークラスとして抽出してしまうと、
継承関係の制約(単一継承しかできない)を受けるためよろしくありません。


なんで、Rubyに用意されているモジュール機能を使います。
Rubyは複数のモジュールをインクルード可能です。
モジュールはJavaのインタフェースに近いような?気がします。
#実装つきのインタフェースってかんじ?


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


Employeeクラス。Subjectモジュールをインクルードしています。

class Employee
  include Subject
  
  attr_reader :name
  attr_accessor :title, :salary

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


以上がObserverパターンのRubyっぽいモジュールを使った方法になります。
ちなみにRubyのライブラリの中にもObservableモジュールが用意されています。


話が少し変わりますが、
RubyでObserverパターンを実現する場合、
コードブロックをObserverクラスに見立てて処理することもできます。
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.call(self)
    end
  end
end

add_observer(&observer)メソッドの引数がコードブロックの参照になっているのと、
notify_observersメソッドの中でObserverとなるコードブロックを実行しています。


呼び出しもとのプログラムはこんなかんじになります。

require "employee"
fred = Employee.new("Fred", "Crane Operator", 30000.0)
fred.add_observer do |changed_employee|
  puts "#{changed_employee.name}のために小切手を切ります。"
  puts "給料は#{changed_employee.salary}です。"
end

fred.salary = 35000.0

fred.add_observerの引数にコードブロックを渡しています。
これがObserverとして機能します。


実行結果はさきほどと同じ。

Fredのために小切手を切ります。
給料は35000.0です。


以上で、Observerパターンおしまい。


ちなみに、コードを読んで、ブログにまとめると大体2時間くらいかかります。
それが勉強になっていると信じてあと10コくらいのパターンの紹介を続けます。


あと、クラス図を簡単に書けるツール(Webで使える)あればいいなー。と。


おしまい。