适用于4.0的Rails Observer替代品

Rails Observer Alternatives for 4.0

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

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

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

就个人而言,我倾向于某种"胖控制器"实现,在每个模型控制器的创建/更新/删除方法中跟踪这些更改。虽然它稍微膨胀了每个控制器的行为,但它确实有助于可读性和理解,因为所有代码都在一个地方。缺点是现在的代码非常相似,分散在几个控制器中。将该代码提取到帮助器方法是一种选择,但您仍然可以调用遍布各处的方法。不是世界末日,也不是"精瘦控制者"的精神。

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

所以在Rails 4中,no-Observers世界,如果你必须在创建/更新/销毁另一条记录之后创建一条新记录,你会使用什么样的设计模式?胖控制器,ActiveRecord回调,还是完全不同的东西?

谢谢。


看看忧虑

在models目录中创建一个名为concern的文件夹。在那里添加一个模块:

1
2
3
4
5
6
7
8
9
10
11
module MyConcernModule
  extend ActiveSupport::Concern

  included do
    after_save :do_something
  end

  def do_something
     ...
  end
end

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

1
2
3
class MyModel < ActiveRecord::Base
  include MyConcernModule
end

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


他们现在在插件中。

我还可以推荐一种替代方案,它可以为您提供以下控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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


我的建议是阅读James Golick的博客文章http://jamesgolick.com/2010/3/14/crazy-heretical-and-awesome-the-way-i-write-rails-apps.html(试着忽略如何标题听起来不谦虚)。

回到那一天,这一切都是"肥胖的模特,瘦小的控制者"。然后脂肪模型成为一个巨大的头痛,特别是在测试期间。最近推出的是瘦模型 - 这个想法是每个类应该处理一个责任而模型的工作就是将数据保存到数据库中。那么我所有复杂的业务逻辑最终会在哪里结束?在业务逻辑类中 - 表示事务的类。

当逻辑开始变得复杂时,这种方法可能变成泥潭(giggity)。这个概念虽然合理 - 而不是使用难以测试和调试的回调或观察器隐式触发事物,而是在模型顶层逻辑层的类中显式触发事物。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

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

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

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

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


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

1
2
3
4
5
6
7
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


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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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("
")
        end
    end
  end

end

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

初始化程序注册观察者:

1
2
User.add_observer(NotificationSender)
User.add_observer(ProfilePictureCreator)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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观察者类定义了各种模型公开的事件的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

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


我认为观察员被弃用的问题并不是观察者本身就是坏人,而是他们被滥用了。

我会提醒你不要在你的回调中添加太多逻辑,或者只是在已经找到Observer模式这个问题的合理解决方案的情况下移动代码来模拟观察者的行为。

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

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

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


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


如何使用PORO代替?

这背后的逻辑是你的"额外保存行动"可能是商业逻辑。我喜欢将两个AR模型(??应该尽可能简单)和控制器(这些都很难以正确测试)分开

1
2
3
4
5
6
7
8
class LoggedUpdater

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

end

简单地称之为:

1
LoggedUpdater.save!(user)

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

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

并举例说明'额外'。你可能想稍微调整一下它们:

1
2
3
4
5
class EmailLogger
  def call(msg)
    #send email with msg
  end
end

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

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

1
2
3
4
5
6
7
8
9
10
class LoggedUpdater

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

end

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

Rails 2.3.2的错误报告


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

1
2
3
4
5
6
7
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