【问题标题】:Is there a more direct way to do a pub/sub pattern in Rails than Observers?在 Rails 中是否有比观察者更直接的方式来实现发布/订阅模式?
【发布时间】:2014-07-25 18:32:00
【问题描述】:

我有一个模型依赖于一个单独的连接模型。

class Magazine < ActiveRecord::Base
  has_one :cover_image, dependent: :destroy, as: :imageable
end

class Image < ActiveRecord::Base
  belongs_to :imageable, polymorphic: true
end

图像是多态的,可以附加到许多对象(页面和文章)上,而不仅仅是杂志。

杂志需要在其相关图像发生任何变化时自行更新

杂志还保存了自己的截图,可以用来宣传:

class Magazine < ActiveRecord::Base
  has_one :cover_image, dependent: :destroy, as: :imageable
  has_one :screenshot

  def generate_screenshot
    # go and create a screenshot of the magazine
  end
end

现在如果图片发生变化,杂志也需要更新截图。所以杂志真的需要知道图像什么时候发生了什么事。

所以我们可以直接从封面图片天真地触发屏幕截图更新

class Image < ActiveRecord::Base
  belongs_to :imageable, polymorphic: true
  after_save { update_any_associated_magazine }

  def update_any_associated_magazine
    # figure out if this belongs to a magazine and trigger
    # screenshot to regenerate
  end
end

...但是图片不应该代表杂志做事

但是,该图像可以用于许多不同的对象,并且确实不应该执行特定于杂志的操作,因为这不是图像的责任担心。图片也可以附加到页面或文章中,不需要为它们做各种事情。

“正常”的 rails 方法是使用观察者

如果我们采用 Rails(y) 方法,那么我们可以创建一个第三方观察者,然后触发相关杂志上的事件:

class ImageObserver < ActiveRecord::Observer
  observe :image

  def after_save image
    Magazine.update_magazine_if_includes_image image
  end
end

但是,这对我来说感觉有点糟糕。

我们通过更新杂志避免了图像的负担,这很棒,但我们实际上只是将问题推到了下游。这个观察者的存在并不明显,在 Magazine 对象内部并不清楚对 Image 的更新实际上会触发对 Magazine 的更新,而且我们有一个奇怪的浮动对象,它的逻辑实际上只属于 Magazine。

我不想要观察者——我只想要一个对象能够订阅另一个对象上的事件。

有没有办法直接从另一个模型订阅一个模型的更改?

我更愿意让杂志直接订阅图片上的事件。所以代码看起来像:

class Magazine < ActiveRecord::Base
  ...

  Image.add_after_save_listener Magazine, :handle_image_after_save

  def self.handle_image_after_save image
    # determine if image belongs to magazine and if so update it
  end
end

class Image < ActiveRecord::Base
  ...

  def self.add_after_save_listener class_name, method
    @@after_save_listeners << [class_name, method]
  end

  after_save :notify_after_save_listeners

  def notify_after_save_listeners
    @@after_save_listeners.map{ |listener|
      class_name = listener[0]
      listener_method = listener[1]
      class_name.send listener_method
    }
end

这是一种有效的方法吗?如果不是,为什么不呢?

这种模式对我来说似乎是明智的。它使用类变量和方法,因此不对特定实例可用做任何假设。

但是,我现在已经足够大,也足够聪明了,我知道如果在 Rails 中还没有完成一些看似显而易见的事情,那可能是有充分理由的。

这对我来说似乎很酷。但它有什么问题?为什么我看到的所有其他解决方案都在第三方反对处理事情?这行得通吗?

【问题讨论】:

  • 我同意观察者和回调...有一个有趣的 gem,叫做 wisper,它做类似的事情。在这里查看:github.com/krisleech/wisper
  • 我之前在 Laravel 中实现了你所说的模式,它非常容易创建并且总体上运行良好。我能想到的唯一缺点是 1)需要有一个绕过机制 2)调试,很难追溯什么时候被调用,而且很容易导致意想不到的后果,但可以通过良好的工具来弥补我想。

标签: ruby-on-rails ruby publish-subscribe


【解决方案1】:

我使用 Redis:

在初始化器中我设置了 Redis:

# config/initializers/redis.rb
uri = URI.parse ENV.fetch("REDISTOGO_URL", 'http://127.0.0.1:6379')
REDIS_CONFIG = { host: uri.host, port: uri.port, password: uri.password }
REDIS = Redis.new REDIS_CONFIG

在开发中它会默认为我的本地 redis 安装,但在 Heroku 上它将使用 Redis To Go。

然后我使用模型回调发布:

class MyModel < ActiveRecord::Base
  after_save { REDIS.publish 'my_channel', to_json }
end

然后我可以从任何地方订阅,例如我用来通过Event Source推送事件的控制器

class Admin::EventsController < Admin::BaseController
  include ActionController::Live

  def show
    response.headers["Content-Type"] = "text/event-stream"

    REDIS.psubscribe params[:event] do |on|
      on.pmessage do |pattern, event, data|
        response.stream.write "event: #{event}\n"
        response.stream.write "data: #{data}\n\n"
      end
    end
  rescue IOError => e
    logger.info "Stream closed: #{e.message}"
  ensure
    redis.quit
    response.stream.close
  end
end

Redis 非常适合灵活的发布/订阅。我在控制器中的代码可以放在任何地方,比如说在初始化程序中:

# config/initializers/subscribers.rb

REDIS.psubscribe "image_update_channel" do |on|
  on.pmessage do |pattern, event, data|
    image = Image.find data['id']
    image.imageable # update that shiz
  end
end

现在它将在您更新图像时处理消息:

class Image < ActiveRecord::Base
  belongs_to :imageable, polymorphic: true
  after_save { REDIS.publish 'image_update_channel', to_json }
end

【讨论】:

  • 这很好。我喜欢它,我喜欢简单的 pub/sub 方法。但是,为什么需要为此去 Redis 呢?除了拥有 SOA,我建议的内存解决方案还有什么缺点?
  • 如果你部署到云端,这会更可靠,比如 Heroku。 Dynos 经常重启,redis 为您提供消息持久性
  • 这很有趣,我已经授予你赏金,因为它绝对是最接近我所期待的。不过,其中仍有一些我不明白的地方 - 您是否推荐一些特定的文章来进一步探索这种方法?
  • @PeterNixey 谢谢。这是一篇很好的文章:robots.thoughtbot.com/redis-pub-sub-how-does-it-work。在使用 redis 和 Rails 搜索 pub sub 时,您还可以找到其他文章。如果仍有任何不清楚的地方,请随时发布另一个问题,我很乐意提供帮助。
  • 谢谢 Diego,非常感谢
【解决方案2】:

Rails 中有ActiveSupport Notifications 机制用于实现发布/订阅。

首先,您应该定义将发布事件的instrument

class Image < ActiveRecord::Base
  ...

  after_save :publish_image_changed

  private

  def publish_image_changed
    ActiveSupport::Notifications.instrument('image.changed', image: self)
  end
end

那么你应该订阅这个事件(你可以把这段代码放在初始化器中):

ActiveSupport::Notifications.subscribe('image.changed') do |*args|
  event = ActiveSupport::Notifications::Event.new(*args)
  image = event.payload[:image]

  # If you have no other cases than magazine, you can check it when you publish event.
  return unless image.imageable.is_a?(Magazine)

  MagazineImageUpdater.new(image.imageable).run
end

【讨论】:

  • 我的理解是通知更多是用于监控rails。另外,除非我遗漏了一些东西,否则这种模式与观察者模式没有什么不同——你仍然需要注册第三方观察者
  • @PeterNixey 这和关注有什么区别?担忧不会抓住事件。他们修改现有的类和实例。
  • @PeterNixey 哎呀,出于某种原因,我将观察者视为关注点。请忽略最后一条评论!诚实的问题:在这里使用观察者模式有什么问题吗?
  • 另外,我意识到这更像是一种 Pub-Sub 模式,而不是 Observer 模式。原因是 Image 类(广播者)对在监听一无所知。
【解决方案3】:

我试试看……

使用 public_send 通知父类发生变化:

class BaseModel < ActiveRecord::Base
  has_one :child_model

  def respond_to_child
    # now generate the screenshot
  end
end


class ChildModel < ActiveRecord::Base                                                                           
  belongs_to :base_model

  after_update :alert_base                                                                                      

  def alert_base                                                                                                
    self.base_model.public_send( :respond_to_child )                                                            
  end                                                                                                           

end

【讨论】:

  • 我不太确定这是如何解决问题的。我认为这有一些问题,但如果我遗漏了什么,我很高兴得到纠正......
  • 再看,好像不需要public_send,孩子直接调用方法就可以了。我将您的问题解释为“如何在子类不知道如何更新的情况下更新基类?”。我确实相信我的答案符合这种解释。
  • 嗨,卡尔-感谢您的回答,我认为您的解释实际上与问题完全不同-它们完全是不同的课程
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-11-09
  • 1970-01-01
  • 1970-01-01
  • 2021-12-26
  • 2016-12-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多