今天让我们来深入了解一下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已经正是你要找的东西。