Menu
CoddyTech

观察者模式

本课属于 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)   # 被观察对象没有变化

观察者也可以是代码块。主题将它们存储为过程对象,并使用 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"]

主题决定要发送什么。传递主题本身可以让观察者读取所需内容;传递事件名称可以让一个观察者响应多种变化:

@observers.each { |o| o.update(self) }          # 观察者读取 subject.price
@observers.each { |o| o.update(:sold_out, self) }  # 观察者检查事件
challenge icon

挑战

简单

完成主题类 Stock 和两个观察者。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 在线编译器