Ruby

【Rubyで解説するデザインパターン】Observer パターン

今回は Observer パターンについて書いていきたいと思います。

参考にしたのは「Rubyによるデザインパターン」の書籍です。もう販売されていなくて中古でしか買えないですが、Ruby プログラマーがデザインパターンを学ぶのにすごいいい本だと思います。

どうしてもデザインパターンは Java や C++ で説明されているものが多く、これらの言語を触ったことがないと読むのが中々しんどかったです。

何となくは分かるんですが詳細が分からず、この本にたどり着きました。

Observer パターンとは

Observerパターンは、主に Subject(発信者)と Observer(消費者)からなる二つのコンポーネントで構成されます。

Subject は、状態の変化が観察されるオブジェクトであり、Observer はその変化に反応するオブジェクトです。

Subject は、一つ以上の Observer を持つことができ、Subject の状態に変更があるたびに、登録された全ての Observer に通知します。

このパターンの機能は、Observer が Subject に対して「登録」することから始まります。

Subject の状態に変更が発生すると、Subject は自身に登録されている全ての Observer に通知し、それぞれの Observer は更新を行うための特定のアクションを実行します。

このメカニズムにより、Subject と Observer 間の疎結合が保たれ、オブジェクト間の相互作用が効率的かつ動的に行われます。

Observer パターンの利点は、オブジェクト間の通信がインターフェースによって明確に定義され、オブジェクト同士が互いに独立しているため、Subject や Observer の一部を変更しても他の部分に影響を与えにくいことです。

実際の例を見たほうが分かりやすいので、例を見ていきます。

Observer パターンを使わない例

今、Employee クラスと Payroll クラス(給与部門)があるとします。Payroll は給料の変更の通知を行います。

class Employee
  attr_reader :name
  attr_accessor :title, :salary

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

class Payroll
  def update(employee)
    puts "Cut a new check for #{employee.name}!"
    puts "The new salary is #{employee.salary}!"
  end
end

給料が変わったときに通知を行うように変更します。

class Employee
  attr_reader :name
  attr_accessor :title, :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 = 40000 # => Cut a new check for Fred! The new salary is 40000!

このコードの問題点は給与の変更通知を Employee クラスにハードコーディングしている点です。

例えば、税務署員も給与の変更について知りたいと言ってきた場合どうなるでしょう。

以下のように Employee クラスに変更を加えないといけません。つまり、Emploee が誰が給与の変更に関して知りたがっているか知っている状態です。

class Employee
  attr_reader :name
  attr_accessor :title, :salary

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

  def salary=(new_salary)
    @salary = new_salary
    @payroll.update(self)
    @tax_man.update(self)
  end
end

他にも通知を必要とするオブジェクトが増えるたびに Employee クラスを変更しなければなりません。

Observer パターンを使う例

Employee は具体的に誰が通知を知りたがっているかは必要なく、必要なのは変更に関心のあるオブジェクトの一覧です。

@observers にオブジェクトの一覧を持つようにして、変更時にはそれらの update メソッドを呼び出すようにします。

それらオブジェクトは update メソッドを必ず持たなければいけません。

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

fred = Employee.new('Fred', 'Crane Operator', 30000)
fred.add_observer(Payroll.new)
fred.salary = 40000 # => Cut a new check for Fred! The new salary is 40000!

これにより、税務署員も給与の変更について知りたいと言ってきた場合でも、Employee クラスを変更する必要はありません。

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

fred = Employee.new('Fred', 'Crane Operator', 30000)
fred.add_observer(Payroll.new)
fred.add_observer(TaxMan.new)
fred.salary = 40000 # => Cut a new check for Fred! The new salary is 40000! Fredに新しい税金の請求書を送ります

オブザーバに対する責務を分離する

オブザーバーに関する責務を持ったコードは分離することができます。こうすることで、他のクラスでオブジェクトを観測する必要がある度に同じコードを書くことをなくせます。

このときに継承とモジュールの2つの方法がありますが、この場合はモジュールの方が適しているのでモジュールにします。

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, :salary

  def initialize(name, title, salary)
    super() # Subject の initialize を呼び出す
    @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)
fred.add_observer(Payroll.new)
fred.salary = 40000

Employee クラスの初期化では Subject の initialize を呼び出すために super() を書いているところに注意してください。

また、Ruby では標準で Observable モジュールが備わっているので、これを使うことができます。

require 'observer'

class Employee
  include Observable

  attr_reader :name, :title, :salary

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

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

fred = Employee.new('Fred', 'Crane Operator', 30000)
fred.add_observer(Payroll.new)
fred.salary = 40000

コードブロックとしてのオブザーバー

コードブロックを使うことでより簡単にオブザーバーに登録することができます。

以下のように Subject をコードブロックように書き換えることで、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.call(self)
    end
  end
end

class Employee
  include Subject

  attr_reader :name, :address, :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)
fred.add_observer do |changed_employee|
  puts "Cut a new check for #{changed_employee.name}!"
  puts "The new salary is #{changed_employee.salary}!"
end
fred.salary = 40000

改めて Observer パターンとは

Gof は「何らかのオブジェクトが変化した」というニュースの発信者(Subject)と消費者(Observer)の間にインターフェースを作ることを Observer パターンと呼んでいます。

上記の例では、Employee が Subject で Payroll が Observer です。Observer がサブジェクトの状態の変化を知ることに関心がある場合(もっとも関心があるから Observer という名前ですが)、その Observer は Subject に登録されます。

Rails プロジェクトでの Observer パターン

Rails 4 までの時代には ActiveRecord::Observer が存在していましたが、現在は削除されています。

理由はモデルのライフサイクルイベントに追加され暗黙的にメソッドが呼ばれることになるので、テストがしにくかったりバグの原因になりやすいからということでした。

今でも Gem として提供はされてますが、もうメンテナンスされていないので避けたほうがよさそうです。

余談ですが、コールバックも同じ理由で嫌う開発者が多いですが、いつかなくなる日がくるんでしょうか。

さいごに

Rails のプロジェクトでモデルのコールバックを使って履歴を保存しているのは見たことあり、このパターンだったのかと勉強になりました。

知っているだけでコードの見方が変わるのでデザインパターンを知っておくことはいいコードを書く上でとても大切だと感じています。

ここまで読んでいただきありがとうございました!

ABOUT ME
sakai
東京在住の30歳。元々は車部品メーカーで働いていてましたが、プログラミングに興味を持ちスクールに通ってエンジニアになりました。 そこからベンチャー → メガベンチャー → 個人事業主になりました。 最近は生成 AI 関連の業務を中心にやっています。 ヒカルチャンネル(Youtube)とワンピースが大好きです!