Ruby标准库中的Observable模块

204 阅读3分钟

今天让我们来深入了解一下Ruby标准库中的Observable模块

火箭的发射

比方说,我们要发射一枚火箭。

幸运的是,我们有一个非常高的抽象层次来处理所有火箭发射的部分。我们只需要将倒计时输出到STDOUT。好的,很简单!

class Countdown
  attr_reader :starting_count

  def initialize(starting_count)
    @starting_count = starting_count
  end

  def run
    starting_count.downto(0) do |count|
      puts count
    end
  end
end

Countdown.new(5).run
$ ruby countdown.rb
5
4
3
2
1
0

完成了!

爆炸了!

但是等等,我们需要在0之后发出一个特殊的 "BLAST OFF"字符串来触发爆炸:

class Countdown
  attr_reader :starting_count

  def initialize(starting_count)
    @starting_count = starting_count
  end

  def run
    starting_count.downto(0) do |count|
      puts count
    end
    puts "BLAST OFF"
  end
end

Countdown.new(5).run
$ ruby countdown.rb
5
4
3
2
1
0
BLAST OFF

好的,完成

点火

但是等等,我们需要在数到3的时候触发点火程序:

class Countdown
  attr_reader :starting_count

  def initialize(starting_count)
    @starting_count = starting_count
  end

  def run
    starting_count.downto(0) do |count|
      puts count
      if count == 3
        puts "IGNITION"
      end
    end
    puts "BLAST OFF"
  end
end

Countdown.new(5).run
$ ruby countdown.rb
5
4
3
!!! IGNITION !!!
2
1
0
BLAST OFF

责任太大

好吧,这可以了,但是我们可怜的简单倒计时类现在有很多责任了。太多的责任!让我们把点火和爆炸的工作转移到其他的类上:

class Countdown
  attr_reader :starting_count,
              :ignition_control,
              :blast_off

  def initialize(starting_count)
    @starting_count = starting_count
    @ignition_control = IgnitionControl.new(3)
    @blast_off = BlastOff.new
  end

  def run
    starting_count.downto(0) do |count|
      puts count
      ignition_control.check(count)
      blast_off.check(count)
    end
  end
end

class IgnitionControl
  attr_reader :ignite_at

  def initialize(ignite_at=0)
    @ignite_at = ignite_at
  end

  def check(count)
    puts "!!! IGNITION !!!" if count == ignite_at
  end
end

class BlastOff
  def check(count)
    puts "BLAST OFF" if count == 0
  end
end

Countdown.new(5).run
$ ruby countdown.rb
5
4
3
!!! IGNITION !!!
2
1
0
BLAST OFF

太多的知识

这很有效,但是我们可怜的倒计时类仍然对它的合作者知道得太多了。它应该只关心倒计时,而不是设置点火和爆炸的类。

与其把合作者硬编码到倒计时中,不如试着在运行时把它们连接起来。

class Countdown
  attr_reader :starting_count, :listeners

  def initialize(starting_count)
    @starting_count = starting_count
    @listeners = []
  end

  def add_listener(listener)
    listeners << listener
  end

  def run
    starting_count.downto(0) do |count|
      puts count
      listeners.each do |listener|
        listener.update(count)
      end
    end
  end
end

class IgnitionControl
  attr_reader :ignite_at

  def initialize(ignite_at=0)
    @ignite_at = ignite_at
  end

  def update(count)
    puts "!!! IGNITION !!!" if count == ignite_at
  end
end

class BlastOff
  def update(count)
    puts "BLAST OFF" if count == 0
  end
end

countdown = Countdown.new(5)
ignition = IgnitionControl.new(3)
blastoff = BlastOff.new

countdown.add_listener(ignition)
countdown.add_listener(blastoff)

countdown.run
$ ruby countdown.rb
5
4
3
!!! IGNITION !!!
2
1
0
BLAST OFF

这样会好一点。我们已经改变了我们对计数有反应的东西的API,并在运行时添加它们,而不是把它们硬编码到倒计时类中。

现在,倒计时类从一个空的监听器列表开始,它有一个方法来允许新的监听器对象被添加。每个监听器都会有一个update 方法,他们可以期望在倒计时的每个数字中被调用。不错啊!

事实证明,通过这种方法,我们几乎已经实现了Observable的最基本版本!我们将切换到真正的东西。

让我们换成真正的东西。既然如此,我们甚至可以让倒计时的STDOUT成为另一个观察者。

可观察的倒计时

require "observer"

class Countdown
  include Observable

  attr_reader :starting_count

  def initialize(starting_count)
    @starting_count = starting_count
  end

  def run
    starting_count.downto(0) do |count|
      changed
      notify_observers count
    end
  end
end

class TerminalOutput
  def update(count)
    puts count
  end
end

class IgnitionControl
  attr_reader :ignite_at
  def initialize(ignite_at=0)
    @ignite_at = ignite_at
  end

  def update(count)
    puts "!!! IGNITION !!!" if count == ignite_at
  end
end

class BlastOff
  def update(count)
    puts "BLAST OFF" if count == 0
  end
end

countdown = Countdown.new(5)
countdown.add_observer(TerminalOutput.new)
countdown.add_observer(IgnitionControl.new(3))
countdown.add_observer(BlastOff.new)
countdown.run
$ ruby countdown.rb
5
4
3
!!! IGNITION !!!
2
1
0
BLAST OFF

万岁!让我们仔细看看使用Observable是什么样子的:

  • 在将要发射信号的类中,我们include Observable 。这就处理了添加数据结构的问题,该结构将包含正在观察的对象。
  • 当我们有一个要发射的更新时,我们调用changed ,它告诉Observable,它应该用通知来实际调用观察者。Observable的一个很好的特点是,只有当类声明了一个变化时,才会发出通知。
  • 我们用数据调用notify_observers

notify_observers 从一个已经声明了change 的可观察对象中被调用时,那么它就会在每个监听器上调用update ,并附上它所得到的任何参数。

# simply call update on the observers
notify_observers

# send :hello to the observers
notify_observers :hello

# send :temperature and the current_temperature variable to the observers
notify_observers :temperature, current_temperature

有了Observable,我们甚至可以很容易地做一些很酷的事情,比如在IGNITION之后跳过输出 "2":

def run
  starting_count.downto(0) do |count|
    changed unless count == 2
    notify_observers count
  end
end
$ ruby countdown.rb
5
4
3
!!! IGNITION !!!
1
0
BLAST OFF

当然,我们又把更多的责任交给了倒计时。但也许倒计时应该知道特定的事件何时发生。谁知道呢?不是我!

许多信号

通过Observable模块,我们可以有几个(或一个)观察者,从很多不同的地方获得通知:

require "observer"

class Sonar
  include Observable
  attr_reader :label

  def initialize(label)
    @label = label
  end

  def ping
    changed
    notify_observers "ping from SONAR #{label}"
  end
end

class Station
  attr_reader :label

  def initialize(label)
    @label = label
  end

  def update(signal)
    puts "Station #{label}: #{signal} detected"
  end
end

station = Station.new(1)

sonars = 1.upto(9999).map do |n|
  Sonar.new(n).tap { |sonar| sonar.add_observer(station) }
end

sonars.each do |sonar|
  sonar.ping
end
$ ruby many_signals.rb | tail
Station 1: ping from SONAR 9990 detected
Station 1: ping from SONAR 9991 detected
Station 1: ping from SONAR 9992 detected
Station 1: ping from SONAR 9993 detected
Station 1: ping from SONAR 9994 detected
Station 1: ping from SONAR 9995 detected
Station 1: ping from SONAR 9996 detected
Station 1: ping from SONAR 9997 detected
Station 1: ping from SONAR 9998 detected
Station 1: ping from SONAR 9999 detected

许多观察者

或者我们可以有很多的观察者都在观察一个对象的通知:

require "observer"

class Sonar
  include Observable
  attr_reader :label

  def initialize(label)
    @label = label
  end

  def ping
    changed
    notify_observers "ping from SONAR #{label}"
  end
end

class Station
  attr_reader :label

  def initialize(label)
    @label = label
  end

  def update(signal)
    puts "Station #{label}: #{signal} detected"
  end
end

sonar = Sonar.new(1)
1.upto(9999) do |n|
  sonar.add_observer(Station.new(n))
end
sonar.ping
$ ruby many_stations.rb | tail
Station 9990: ping from SONAR 1 detected
Station 9991: ping from SONAR 1 detected
Station 9992: ping from SONAR 1 detected
Station 9993: ping from SONAR 1 detected
Station 9994: ping from SONAR 1 detected
Station 9995: ping from SONAR 1 detected
Station 9996: ping from SONAR 1 detected
Station 9997: ping from SONAR 1 detected
Station 9998: ping from SONAR 1 detected
Station 9999: ping from SONAR 1 detected

很多很多?都很好

或者我们可以有很多的观察者观察很多的可观察对象

require "observer"

class Sonar
  include Observable
  attr_reader :label

  def initialize(label)
    @label = label
  end

  def ping
    changed
    notify_observers "ping from SONAR #{label}"
  end
end

class Station
  attr_reader :label

  def initialize(label)
    @label = label
  end

  def update(signal)
    puts "Station #{label}: #{signal} detected"
  end
end

COUNT = 100

stations = COUNT.times.map { |n| Station.new(n) }

sonars = COUNT.times.map do |n|
  Sonar.new(n).tap do |sonar|
    stations.each do |station|
      sonar.add_observer(station)
    end
  end
end

sonars.each { |sonar| sonar.ping }
$ ruby many_many.rb | tail
Station 90: ping from SONAR 99 detected
Station 91: ping from SONAR 99 detected
Station 92: ping from SONAR 99 detected
Station 93: ping from SONAR 99 detected
Station 94: ping from SONAR 99 detected
Station 95: ping from SONAR 99 detected
Station 96: ping from SONAR 99 detected
Station 97: ping from SONAR 99 detected
Station 98: ping from SONAR 99 detected
Station 99: ping from SONAR 99 detected

到处都是信号!哇!

Observable模块是Ruby标准库中的一个伟大工具。如果你发现自己在编写允许对象对其他对象的变化做出反应的机制,你很可能发现Observable已经正是你要找的东西。