Ruby 在 Ruby 中实现一个信号订阅通知功能 (一)

qichunren · December 13, 2019 · Last by luikore replied at December 20, 2019 · 5014 hits

为了实现模块之间的解耦,我需要一个类似 Qt 中的信号槽机制 和 GDScript 中的信号机制​​​​​​​

使用场景:

有一个计数器和一个显示屏,当计数器更新时,需要显示屏同步显示更新的计数,后续可能需要将计数器的计数用于到其它地方

先看看在没有实现信号订阅机制时的代码是怎么样写的。

class Counter
    attr_accessor :value

    def initialize(led_screen)
        @led_screen = led_screen
    end

    def increment
        @value = @value + 1
        @led_screen.display(@value)
    end
end

class LedScreen

    def display(content)
        # 显示到屏上,暂无实现
    end
end

led_screen = LedScreen.new
counter = Counter.new(led_screen)
counter.increment

以上代码可以基本工作,但是我对这样的代码不满意。在对计数器 Counter 类进行单元测试时,需要引入 LedScreen 类,增加了复杂性。另外当有新的需求时,需要修改 Counter#increment 方法。

我不喜欢这样,我准备使用信号机制实现解耦。当需要将计数器 Counter 的值显示到其它地方,也不需要改动 Counter 的代码,计数器 Counter 就只是做它本应该做的事情。

首先不考虑具体的实现,先把我设想的 API 接口初步定下来,后续考虑使用 Ruby 的魔法来实现出来。用下面的代码来说明:

class Counter
    attr_accessor :value
    register_signal :value_changed

    def increment
        @value = @value + 1
        emit_signal :value_changed, @value
        # emit_signal 第0个参数是信号名,第1个参数是附带的消息
    end
end

class LedScreen
    # 默认调用 on_value_changed
    # 也可以附加一个参数用来指定信号订阅的方法
    # eg: subscribe_signal :value_changed, :when_value_changed
    subscribe_signal :value_changed 

    def on_value_changed(value)
        puts "Got counter value changed to #{value}"
        # 更新到显示屏上
        display(value)
    end

    def display(content)
        # 显示到屏上,暂无实现
    end
end

# Testcase

counter = Counter.new
counter.increment
led_screen = LedScreen.new
# 期望 LedScreen#on_value_changed 方法被调用

待续。也希望了解一下大家的想法。

消息队列

这就是个观察者模式撒,ruby 标准库不是提供了 observable 模块么。你的 emit_signal 接口对应的就是库里的 notify_observers 方法,再在订阅者里调用 add_observer 并实现 update 方法就完事了撒

看看 gem wisper 用起来和你的接口设计基本上是一样的

Qt 还有一步 connect counter, :onchange, led_screen, :onchange 你没写。

Qt 这么做其实很绕,原因是传递函数的话,类型写起来会很复杂,所以才用 signal / slot 的方式解耦合。

然而 Ruby method call 就已经是 signal / slot 机制了。简单实现 (也可以抽出个 connect() 函数做点元编程):

class Counter
  def initialize
    @onchange = []
  end
  def onchange method
    @onchange << method
  end
  def increment
    @value.succ!
    @onchange.each {|m| m.(@value) }
  end
end

class LedScreen
  def onchange v
    @value = v
    display
  end
  def display
  end
end

counter = Counter.new
led_screen = LedScreen.new
counter.onchange led_screen.method :display

当然上面的实现有个问题,就是事件触发没有异步化,在事件响应的触发点,把 method call 扔到别的执行线程就完整了。

Cocoa 也有类似机制,叫 responder

You need to Sign in before reply, if you don't have an account, please Sign up first.