JQ Blog

RubyにおけるObserverパターン

Observerパターン

サンプル①

コード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Payroll
  def update(changed_employee)
    puts "#{changed_employee.name}のために小切手を切ります!"
    puts "彼の給料はいま#{changed_employee.salary}です!"
  end
end

class Employee
  attr_reader :name
  attr_accessor :salary

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

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

結果

1
2
3
4
5
6
7
payroll = Payroll.new
jo = Employee.new('Jo', 30000, payroll)
jo.salary = 35000

# =>
Joのために小切手を切ります!
彼の給料はいま35000です!

これはPayrollupdateだけ指定されていて汎用的ではない。

サンプル②

コード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Payroll
  def update(changed_employee)
    puts "#{changed_employee.name}のために小切手を切ります!"
    puts "彼の給料はいま#{changed_employee.salary}です!"
  end
end

class Employee
  attr_reader :name
  attr_accessor :salary

  def initialize(name, salary)
    @name = name
    @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

結果

1
2
3
4
5
6
7
8
9
10
jo = Employee.new('Jo', 30000)

payroll = Payroll.new
jo.add_observer(payroll)

jo.salary=35000

# =>
Joのために小切手を切ります!
彼の給料はいま35000です!

このコードに新しいクラスを出してそのクラスも適用してみると、

1
2
3
4
5
class TaxMan
  def update(changed_employee)
    puts "#{changed_employee.name}に新しい税金の請求書を送ります!"
  end
end
1
2
3
4
5
6
7
8
9
tax_man = TaxMan.new
jo.add_observer(tax_man)

jo.salary = 90000

# =>
Joのために小切手を切ります!
彼の給料はいま90000です!
Joに新しい税金の請求書を送ります!

サンプル③(モジュール化)

コード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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, :salary
  def initialize(name, salary)
    super()
    @name = name
    @salary = salary
  end

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

結果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
jo = Employee.new('Jo', 30000)

payroll = Payroll.new
tax_man = TaxMan.new

jo.add_observer(payroll)
jo.add_observer(tax_man)

jo.salary = 90000

# =>
Joのために小切手を切ります!
彼の給料はいま90000です!
Joに新しい税金の請求書を送ります!

サンプル④(コードブロック)

RubyでObservableというライブラリを提供してくれるけど、Observableはコードブロックをサポートしてくれない。
次のコードはコードブロックに対する対応になる。

コード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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, :salary
  def initialize(name, salary)
    super()
    @name = name
    @salary = salary
  end

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

結果

1
2
3
4
5
6
7
8
9
10
11
12
jo = Employee.new('Jo', 30000)

jo.add_observer do |changed_employee|
  puts "#{changed_employee.name}のために小切手を切ります!"
  puts "彼の給料はいま#{changed_employee.salary}です!"
end

jo.salary = 90000

# =>
Joのために小切手を切ります!
彼の給料はいま90000です!

RailsのActiveRecord::Observer

RailsではActiveRecord::Observerを提供している。
rails-observersというGemを通して使用できる。

インストール

1
gem 'rails-observers'
1
bundle install

or

1
$ gem install rails-observers

使い方

nameカラムを持っているUserというモデルがあるとしてこのUserモデルに対するObserverを作成してみる。

1
$ rails g observer user

すると、app/models/user_observer.rbというルートで生成される。
そのファイルにクラスを定義する。

1
2
3
4
5
class UserObserver < ActiveRecord::Observer
  def after_save(user)
    puts "#{user.name}が保存されました。"
  end
end

あと、application.rbに設定する。

1
config.active_record.observers = :user_observer

observerが複数の場合、

1
config.active_record.observers = [:user_observer, :team_observer]

のようにもできる。

結果

1
2
3
4
5
user = User.new(name: 'Jo')
user.save

# =>
Joが保存されました。

その他

1
2
3
4
5
6
7
class NotificationsObserver < ActiveRecord::Observer
  observe :comment, :like

  def after_create(record)
    # notifiy users of new comment or like
  end
end

observeを指定すると指定されたモデルのみobserveすることもできる。

終わり

RubyのObserverパターンとActiveRecord::Observerを調べながら
Railsで提供しているライブラリActiveRecord::Callbacksと少し似ていると思った。

参考

本 - Rubyによるデザインパターン
https://github.com/rails/rails-observers