Menu
Coddy logo textTech

オブザーバーパターン

CoddyのRubyジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 44/57。

オブザーバーパターンを使うと、オブジェクトは別のオブジェクトであるサブジェクトの変更を購読できます。サブジェクトはオブザーバーのリストを保持し、何かが起きると、それぞれが何をするかを知らなくても、各オブザーバーに通知します。

class Thermometer
  def initialize
    @observers = []
  end

  def subscribe(observer)
    @observers << observer
  end

  def temperature=(value)
    @temperature = value
    @observers.each { |o| o.update(value) }
  end
end

class Display
  def update(value)
    puts "display: #{value} C"
  end
end

class Alarm
  def update(value)
    puts "alarm: too hot!" if value > 30
  end
end

t = Thermometer.new
t.subscribe(Display.new)
t.subscribe(Alarm.new)
t.temperature = 25
t.temperature = 35

出力:

display: 25 C
display: 35 C
alarm: too hot!

オブザーバーに必要なのは update メソッドだけです。これがダックタイピングです。それぞれがまったく異なるクラスでもよく、新しいオブザーバーをサブジェクトを変更せずに追加できます。

class Logger2
  def update(value)
    @history = (@history || []) << value
  end
end

thermometer.subscribe(Logger2.new)   # サブジェクトは変更されていません

オブザーバーはブロックにすることもできます。対象はそれらを proc として保存し、call を使って呼び出します。unsubscribe はオブザーバーを再び削除します:

class Button
  def initialize
    @handlers = []
  end

  def on_click(&handler)
    @handlers << handler
    handler
  end

  def unsubscribe(handler)
    @handlers.delete(handler)
  end

  def click
    @handlers.map { |h| h.call("clicked") }
  end
end

b = Button.new
b.on_click { |e| "A got #{e}" }
second = b.on_click { |e| "B got #{e}" }
p b.click
b.unsubscribe(second)
p b.click

出力:

["A got clicked", "B got clicked"]
["A got clicked"]

何を送るかはサブジェクトが決定します。サブジェクト自体を渡すと、オブザーバーは必要な情報を読み取れます。一方、イベント名を渡すと、1つのオブザーバーで複数種類の変更に対応できます:

@observers.each { |o| o.update(self) }          # オブザーバーが subject.price を読み取る
@observers.each { |o| o.update(:sold_out, self) }  # オブザーバーがイベントをチェックする
challenge icon

チャレンジ

簡単

Stockクラスと2つのオブザーバーを完成させてください。Stock.new(symbol)にはsubscribe(observer)とupdate_price(value)があり、価格を保存し、すべてのオブザーバーに対するupdate(stock)のnilではない返信を、購読順に返します。PriceDisplay#updateはAAPL now 12.50を返します。PriceAlert.new(limit)は、価格がその上限以下から上限を超えて上昇したときにALERT AAPL above 100.00を返し、それ以外の場合はnilを返します。

最初の入力行にはシンボルとアラートの上限が含まれます。提供されたコードは表示とアラートを購読させ、その後、すべてのset price行に対する返信を出力します。

コードはstock.rb、price_display.rb、price_alert.rbに記述してください。main.rbには提供された入出力コードが含まれており、編集できません。

自分で試してみよう

require_relative 'stock'
require_relative 'price_display'
require_relative 'price_alert'

# 提供された入出力コード: そのままにしておく
input = []
while (line = gets)
  input << line.chomp
end
symbol, limit = input[0].split
stock = Stock.new(symbol)
stock.subscribe(PriceDisplay.new)
stock.subscribe(PriceAlert.new(limit.to_f))
input.drop(1).each do |line|
  stock.update_price(line.split[1].to_f).each { |reply| puts reply }
end
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン

自分で練習してみよう: Rubyオンラインコンパイラ