适用于4.0的Rails Observer替代品


153

随着Observers正式从Rails 4.0中删除,我很好奇其他开发人员正在使用什么。(除了使用提取的宝石。)虽然观察者当然被滥用并且有时很容易变得笨拙,但除了清除缓存之外,还有许多用例,它们是有益的。

以需要跟踪模型更改的应用程序为例。观察者可以轻松地监视模型A上的更改,并使用模型B将这些更改记录在数据库中。如果要监视多个模型之间的更改,则可以由一个观察员来处理。

在Rails 4中,我很好奇其他开发人员正在使用哪些策略代替Observers重新创建该功能。

就我个人而言,我倾向于一种“胖控制器”实现,其中在每个模型控制器的create / update / delete方法中跟踪这些更改。尽管它稍微使每个控制器的行为slightly肿,但由于所有代码都在一个地方,因此确实有助于提高可读性和理解力。不利的一面是,现在有一些非常相似的代码散布在多个控制器中。将代码提取到帮助器方法中是一种选择,但是您仍然需要在各处随处调用这些方法。不是世界末日,也不是完全符合“瘦控制器”的精神。

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

因此,在Rails 4的“无观察员”世界中,如果必须在创建/更新/销毁另一条记录之后创建新记录,那么您将使用哪种设计模式?胖控制器,ActiveRecord回调或其他功能?

谢谢。


4
我真的很惊讶,这个问题没有更多答案了。有点令人不安。
courtsimas

Answers:


82

看看问题

在您的模型目录中创建一个名为担忧的文件夹。在此处添加模块:

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

根据您正在执行的操作,这可能会使您在没有观察员的情况下与您保持联系。


20
这种方法存在问题。值得注意的是,它不会清理您的模型。include将方法从模块复制回您的类。将类方法提取到模块中可以按关注程度将它们分组,但是类仍然still肿。
史蒂文·索罗卡

15
标题是“ 4.0的Rails Observer Alternatives”,而不是“如何最大程度地减少膨胀”。史蒂芬怎么做才干呢?不,这表明“膨胀”是无法替代观察者的原因还不够好。您将不得不提出更好的建议,以帮助社区或解释为什么关注点无法代替观察员。希望您能同时声明= D
UncleAdam 2014年

10
膨胀总是一个问题。更好的替代方法是wisper,如果正确实施,则允许您通过将问题提取到与模型没有紧密联系的单独的类中来清理问题。这也使得隔离测试变得容易
得多

4
通过引入宝石来模型膨胀或整个应用膨胀,我们可以将其设置为个人喜好。感谢您的额外建议。
UncleAdam14年

这只会使IDE的方法自动完成菜单肿,这对许多人来说应该没问题。
lulalala 2015年

33

他们现在在插件中

我还可以推荐一种替代方法吗?

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呢?
2014年

@svoop ActiveSupport::Notifications适用于检测,而不是常规sub / pub。
克里斯(Kris)2014年

@Kris-你是对的。它主要用于检测,但是我想知道是什么阻止了它被用作pub / sub的通用方法?它提供了基本的构建块,对吗?换句话说,与之相比,悄悄话有哪些优点/缺点ActiveSupport::Notifications
gingerlime 2014年

我用的不是Notifications很多,但是我想说Wisper有一个更好的API和诸如“全局订户”,“前缀上”和“事件映射”之类的功能,Notifications但没有。的未来版本Wisper还将允许通过SideKiq / Resque / Celluloid进行异步发布。同样,潜在地,在将来的Rails发行版中,用于该API的API Notifications可能会更改为更加注重仪表。
克里斯(Kris)

21

我的建议是阅读James Golick的博客文章,网址为http://jamesgolick.com/2010/3/14/crazy-heretical-and-awesome-the-way-i-write-rails-apps.html(尝试忽略听起来不恰当)。

在过去,这全都是“胖模特,瘦瘦的控制器”。然后,胖模型变得非常头痛,尤其是在测试期间。最近,推动力是针对瘦模型的-想法是每个班级都要承担一个责任,而模型的工作是将数据持久保存到数据库中。那么,我所有复杂的业务逻辑都在哪里结束?在业务逻辑类中-表示事务的类。

当逻辑开始变得复杂时,这种方法可能会陷入困境。尽管这个概念很合理-而不是使用难以测试和调试的回调或观察器隐式触发事物,而是在将逻辑置于模型顶部的类中显式触发事物。


4
在过去的几个月中,我一直在为一个项目做这样的事情。您最终会得到很多小的服务,但是测试和维护它的简便性绝对胜过缺点。我在这个中型系统上的相当广泛的规格仍然只需要5秒钟即可运行:)
Luca Spiller

也称为PORO(普通的旧Ruby对象)或服务对象
Cyril Duchon-Doris

13

使用活动记录回调只需翻转耦合的依赖性。举例来说,如果你有modelACacheObserver观测modelA轨道3的风格,你可以删除CacheObserver,没有问题。现在,改为说A必须手动调用CacheObserverafter save,这将是rails4。您只需移动依赖项即可安全删除,A但不能删除CacheObserver

现在,从我的象牙塔中,我希望观察者依赖于它所观察的模型。我是否足够在意使控制器混乱?对我来说,答案是否定的。

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

对于依赖控制器行为的任何类型的观察者,我也有一种厌恶(我认为是有根据的)。突然,您必须将观察者注入可能更新您要观察的模型的任何控制器操作(或其他模型)中。如果您可以保证您的应用程序只能通过创建/更新控制器操作来修改实例,则将为您提供更多功能,但这并不是我对Rails应用程序所做的假设(考虑嵌套表单,模型业务逻辑更新关联等)。


1
感谢您的评论@agmin。如果有更好的设计模式,我很乐意放弃使用Observer。我对其他人如何构建代码和依赖关系以提供类似功能(不包括缓存)最感兴趣。就我而言,我想在模型的属性更新时记录对模型的更改。我曾经使用观察者来做到这一点。现在,我试图在胖控制器,AR回调或其他我没有想到的东西之间做出选择。目前,两者似乎都不优雅。
kennyc

13

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

9

在某些情况下,我只是使用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

4

我替代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回调之外,每个模型都可以定义自己的可观察事件。例如,我的用户模型公开了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)具有名称与事件匹配的方法。正如人们可能期望的那样,多个观察者可以注册同一事件,并且(参考原始问题的第二段)观察者可以监视多个模型中的事件。

下面的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

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


3

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

我会提醒您,在已经有一个很好的解决方案“观察者”模式的解决方案中,不要在回调函数中添加太多逻辑,也不要仅仅移动代码来模拟观察者的行为。

如果使用观察者有意义,则一定要使用观察者。只需了解您将需要确保您的观察者逻辑遵循声音编码惯例(例如SOLID)即可。

如果您想将其添加回项目https://github.com/rails/rails-observers,可以在rubygems上使用观察者gem。

看到这个简短的主题,虽然没有进行全面的讨论,但我认为基本论点是有效的。 https://github.com/rails/rails-observers/issues/2



2

改用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


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.