Rails Observer 4.0 的替代品

Posted

技术标签:

【中文标题】Rails Observer 4.0 的替代品【英文标题】:Rails Observer Alternatives for 4.0 【发布时间】:2013-02-16 09:33:54 【问题描述】:

有了正式的观察者removed from Rails 4.0,我很好奇其他开发人员正在使用什么来代替他们。 (除了使用提取的 gem。)虽然观察者肯定被滥用并且有时很容易变得笨拙,但除了缓存清除之外,还有许多有用的用例。

以需要跟踪模型更改的应用程序为例。观察者可以轻松地观察模型 A 的变化,并在数据库中记录模型 B 的变化。如果你想观察多个模型的变化,那么一个观察者就可以处理。

在 Rails 4 中,我很好奇其他开发人员使用什么策略来代替观察者来重新创建该功能。

就个人而言,我倾向于一种“胖控制器”实现,在每个模型控制器的创建/更新/删除方法中跟踪这些更改。虽然它略微夸大了每个控制器的行为,但它确实有助于提高可读性和理解性,因为所有代码都在一个地方。缺点是现在有非常相似的代码分散在多个控制器中。将该代码提取到辅助方法中是一种选择,但您仍然需要调用那些到处都是的方法。不是世界末日,但也不完全符合“瘦控制器”的精神。

ActiveRecord 回调是另一种可能的选择,尽管我个人不喜欢它,因为在我看来,它倾向于将两个不同的模型过于紧密地耦合在一起。

所以在 Rails 4,没有观察者的世界里,如果你必须在另一个记录被创建/更新/销毁之后创建一个新记录,你会使用什么设计模式?胖控制器,ActiveRecord 回调,还是其他的东西?

谢谢。

【问题讨论】:

我真的很惊讶没有为这个问题发布更多答案。有点令人不安。 github.com/krisleech/wisper 【参考方案1】:

看看Concerns

在模型目录中创建一个名为关注点的文件夹。在那里添加一个模块:

module MyConcernModule
  extend ActiveSupport::Concern

  included do
    after_save :do_something
  end

  def do_something
     ...
  end
end

接下来,将其包含在您希望在其中运行 after_save 的模型中:

class MyModel < ActiveRecord::Base
  include MyConcernModule
end

根据您正在做的事情,这可能会让您在没有观察者的情况下接近。

【讨论】:

这种方法存在问题。值得注意的是,它不会清理您的模型; include 将模块中的方法复制回您的类。将类方法提取到模块中可能会按关注点对它们进行分组,但类仍然是臃肿的。 标题是“Rails Observer Alternatives for 4.0”而不是“我如何最小化膨胀”。史蒂文的担忧怎么办?不,暗示“臃肿”是它不能替代观察者的一个原因还不够好。您必须提出更好的建议来帮助社区或解释为什么关注点不能替代观察者。希望你能同时说明 =D 膨胀总是一个问题。更好的选择是wisper,如果实施得当,您可以通过将关注点提取到与模型不紧密耦合的单独类来清理关注点。这也使得单独测试变得更加容易 模型膨胀或整个应用膨胀通过拉入一个 Gem 来做到这一点 - 我们可以让它取决于个人喜好。感谢您的额外建议。 它只会膨胀IDE的方法自动完成菜单,这对很多人来说应该没问题。【参考方案2】:

他们现在在plugin。

我还可以推荐an alternative,它会给你这样的控制器:

class PostsController < ApplicationController
  def create
    @post = Post.new(params[:post])

    @post.subscribe(PusherListener.new)
    @post.subscribe(ActivityListener.new)
    @post.subscribe(StatisticsListener.new)

    @post.on(:create_post_successful)  |post| redirect_to post 
    @post.on(:create_post_failed)      |post| render :action => :new 

    @post.create
  end
end

【讨论】:

ActiveSupport::Notifications 怎么样? @svoop ActiveSupport::Notifications 面向仪表,而不是通用 sub/pub。 @Kris - 你是对的。它主要用于检测,但我想知道是什么阻止它被用作发布/订阅的通用方法?它确实提供了基本的构建块,对吧?换句话说,与ActiveSupport::Notifications 相比,wisper 的优点/缺点是什么? 我没怎么用过Notifications 但我想说Wisper 有更好的API 和诸如'全局订阅者'、'前缀'和'事件映射'之类的功能@987654328 @ 才不是。 Wisper 的未来版本还将允许通过 SideKiq/Resque/Celluloid 进行异步发布。此外,在未来的 Rails 版本中,Notifications 的 API 可能会更改为更加注重仪表。【参考方案3】:

我的建议是阅读 James Golick 在http://jamesgolick.com/2010/3/14/crazy-heretical-and-awesome-the-way-i-write-rails-apps.html 的博客文章(尽量忽略标题听起来多么不正经)。

在过去,一切都是“胖模特,瘦瘦的控制器”。然后胖模型变得非常头疼,尤其是在测试期间。最近推动的是瘦模型——这个想法是每个类都应该处理一个职责,而模型的工作是将数据持久化到数据库中。那么我所有复杂的业务逻辑都到哪里去了?在业务逻辑类中——代表事务的类。

当逻辑开始变得复杂时,这种方法可能会变成一个泥潭(笨拙)。不过这个概念是合理的——与其使用难以测试和调试的回调或观察者隐式触发事物,不如在一个在模型之上分层逻辑的类中显式地触发事物。

【讨论】:

在过去的几个月里,我一直在为一个项目做这样的事情。你最终会得到很多小服务,但测试和维护的便利性肯定超过了缺点。我对这个中型系统的相当广泛的规格仍然只需要 5 秒即可运行 :) 也称为 PORO(Plain Old Ruby Objects)或服务对象【参考方案4】:

使用活动记录回调只是翻转了耦合的依赖关系。例如,如果您有 modelACacheObserver 观察 modelA rails 3 样式,则可以毫无问题地删除 CacheObserver。现在,改为说A 必须在保存后手动调用CacheObserver,这将是rails 4。您只是移动了依赖项,因此您可以安全地删除A,而不是CacheObserver

现在,从我的象牙塔来看,我更喜欢观察者依赖于它所观察的模型。我是否足够在意弄乱我的控制器?对我来说,答案是否定的。

大概你已经思考了为什么你想要/需要观察者,因此创建一个依赖于观察者的模型并不是一个可怕的悲剧。

对于依赖控制器动作的任何类型的观察者,我也有(我认为是有理由的)厌恶。突然间,您必须将观察者注入任何可能更新您想要观察的模型的控制器操作(或另一个模型)中。如果您可以保证您的应用程序只会通过创建/更新控制器操作来修改实例,那么您将获得更多权力,但这不是我对 Rails 应用程序所做的假设(考虑嵌套表单、模型业务逻辑更新关联等)

【讨论】:

感谢 cmets @agmin。如果有更好的设计模式,我很高兴不再使用观察者。我最感兴趣的是其他人如何构建他们的代码和依赖关系以提供类似的功能(不包括缓存)。就我而言,我想在模型属性更新时记录对模型的更改。我曾经使用观察者来做到这一点。现在我正在尝试在胖控制器、AR 回调或其他我没有想到的东西之间做出决定。目前两者似乎都不优雅。【参考方案5】:

Wisper 是一个很好的解决方案。我个人对回调的偏好是它们由模型触发,但事件仅在请求进来时才被监听,即我不希望在测试等中设置模型时触发回调,但我确实想要它们每当涉及控制器时触发。使用 Wisper 很容易设置,因为您可以告诉它只监听块内的事件。

class ApplicationController < ActionController::Base
  around_filter :register_event_listeners

  def register_event_listeners(&around_listener_block)
    Wisper.with_listeners(UserListener.new) do
      around_listener_block.call
    end
  end        
end

class User
  include Wisper::Publisher
  after_create |user| publish(:user_registered, user) 
end

class UserListener
  def user_registered(user)
    Analytics.track("user:registered", user.analytics)
  end
end

【讨论】:

【参考方案6】:

在某些情况下,我只是使用Active Support Instrumentation

ActiveSupport::Notifications.instrument "my.custom.event", this: :data do
  # do your stuff here
end

ActiveSupport::Notifications.subscribe "my.custom.event" do |*args|
  data = args.extract_options! # :this=>:data
end

【讨论】:

【参考方案7】:

我对 Rails 3 Observers 的替代方案是手动实现,它利用模型中定义的回调但设法(如 agmin 在上面的回答中所述)“翻转依赖项...耦合”。

我的对象继承自提供注册观察者的基类:

class Party411BaseModel

  self.abstract_class = true
  class_attribute :observers

  def self.add_observer(observer)
    observers << observer
    logger.debug("Observer #observer.name added to #self.name")
  end

  def notify_observers(obj, event_name, *args)
    observers && observers.each do |observer|
    if observer.respond_to?(event_name)
        begin
          observer.public_send(event_name, obj, *args)
        rescue Exception => e
          logger.error("Error notifying observer #observer.name")
          logger.error e.message
          logger.error e.backtrace.join("\n")
        end
    end
  end

end

(当然,本着组合重于继承的精神,上面的代码可以放在一个模块中并混合在每个模型中。)

初始化器注册观察者:

User.add_observer(NotificationSender)
User.add_observer(ProfilePictureCreator)

除了基本的 ActiveRecord 回调之外,每个模型都可以定义自己的可观察事件。例如,我的 User 模型公开了 2 个事件:

class User < Party411BaseModel

  self.observers ||= []

  after_commit :notify_observers, :on => :create

  def signed_up_via_lunchwalla
    self.account_source == ACCOUNT_SOURCES['LunchWalla']
  end

  def notify_observers
    notify_observers(self, :new_user_created)
    notify_observers(self, :new_lunchwalla_user_created) if self.signed_up_via_lunchwalla
  end
end

任何希望接收这些事件通知的观察者只需要 (1) 向公开事件的模型注册并 (2) 拥有一个名称与事件匹配的方法。正如人们所预料的那样,多个观察者可以注册同一个事件,并且(参考原始问题的第 2 段)观察者可以跨多个模型观察事件。

下面的 NotificationSender 和 ProfilePictureCreator 观察者类为各种模型公开的事件定义了方法:

NotificationSender
  def new_user_created(user_id)
    ...
  end

  def new_invitation_created(invitation_id)
    ...
  end

  def new_event_created(event_id)
    ...
  end
end

class ProfilePictureCreator
  def new_lunchwalla_user_created(user_id)
    ...
  end

  def new_twitter_user_created(user_id)
    ...
  end
end

需要注意的是,在所有模型中公开的所有事件的名称必须是唯一的。

【讨论】:

【参考方案8】:

我认为观察者被弃用的问题不是观察者本身不好,而是他们被滥用了。

我会告诫不要在你的回调中添加过多的逻辑,或者只是移动代码来模拟观察者的行为,因为观察者模式已经有一个很好的解决方案来解决这个问题。

如果使用观察者有意义,那么一定要使用观察者。只需了解您将需要确保您的观察者逻辑遵循正确的编码实践,例如 SOLID。

如果您想将观察者 gem 添加回您的项目,可以在 ruby​​gems 上使用它 https://github.com/rails/rails-observers

看到这个简短的线程,虽然不是完整的全面讨论,但我认为基本论点是有效的。 https://github.com/rails/rails-observers/issues/2

【讨论】:

【参考方案9】:

你可以试试https://github.com/TiagoCardoso1983/association_observers。它尚未针对 rails 4(尚未发布)进行测试,需要更多协作,但您可以检查它是否适合您。

【讨论】:

【参考方案10】:

用 PORO 代替怎么样?

这背后的逻辑是您的“保存时的额外操作”很可能是业务逻辑。我喜欢将其与 AR 模型(应该尽可能简单)和控制器(正确测试很麻烦)分开

class LoggedUpdater

  def self.save!(record)
    record.save!
    #log the change here
  end

end

简单地这样称呼它:

LoggedUpdater.save!(user)

您甚至可以通过注入额外的保存后操作对象来扩展它

LoggedUpdater.save(user, [EmailLogger.new, MongoLogger.new])

并举一个“附加”的例子。不过,您可能想稍微修饰一下:

class EmailLogger
  def call(msg)
    #send email with msg
  end
end

如果您喜欢这种方法,我建议您阅读 Bryan Helmkamps 7 Patterns 博客文章。

编辑:我还应该提到,上述解决方案还允许在需要时添加事务逻辑。例如。使用 ActiveRecord 和支持的数据库:

class LoggedUpdater

  def self.save!([records])
    ActiveRecord::Base.transaction do
      records.each(&:save!)
      #log the changes here
    end
  end

end

【讨论】:

【参考方案11】:

值得一提的是,Ruby 标准库中的 Observable 模块不能用于类似活动记录的对象,因为实例方法 changed?changed 会与来自 ActiveModel::Dirty 的方法发生冲突。

Bug report for Rails 2.3.2

【讨论】:

【参考方案12】:

我有同样的问题!我找到了一个解决方案 ActiveModel::Dirty,这样您就可以跟踪您的模型更改!

include ActiveModel::Dirty
before_save :notify_categories if :data_changed? 


def notify_categories
  self.categories.map!|c| c.update_results(self.data)
end

http://api.rubyonrails.org/classes/ActiveModel/Dirty.html

【讨论】:

以上是关于Rails Observer 4.0 的替代品的主要内容,如果未能解决你的问题,请参考以下文章

Facebook iOS SDK > 4.0 替代 initWithAppId?

Ruby on Rails 的 WordPress 替代品都有哪些? [关闭]

指南针精灵生成器有啥好的替代品吗? (Rails 精灵生成器)

rails minitest 存根断言替代方法

除了 $lookup 运算符之外,MongoDB 4.0 中加入的替代方法是啥,因为它不适用于分片集合

Rails:如何在多部分/替代电子邮件中使用部分(HTML 和纯文本)