Rails Observer 4.0的替代方案

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

以需要跟踪对模型的更改的应用程序为例。观察者可以轻松地监视模型 A 上的更改,并在数据库中用模型 B 记录这些更改。如果您想观察几个模型之间的变化,那么一个观察者就可以处理这个问题。

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

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

ActiveRecord 回调是另一种可能的选择,尽管我个人并不喜欢这种方式,因为在我看来,它往往会将两种不同的模型紧密地结合在一起。

因此,在 Rails 4中,没有观察者的世界中,如果您必须在创建/更新/销毁另一个记录之后创建一个新记录,那么您将使用什么样的设计模式?脂肪控制器、 ActiveRecord 回调,还是其他完全不同的东西?

谢谢你。

36565 次浏览

使用活动记录回调只是简单地翻转耦合的依赖关系。例如,如果您有 modelACacheObserver观察 modelA轨道3样式,您可以删除 CacheObserver没有问题。现在,假设 A在保存之后必须手动调用 CacheObserver,也就是 Rails 4。您只是移动了依赖项,这样就可以安全地删除 A而不是 CacheObserver

现在,在我的象牙塔里,我更喜欢观察者依赖于它所观察的模型。我关心到把我的手柄弄得乱七八糟吗?对我来说,答案是否定的。

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

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

他们现在在 插件

我是否也可以推荐 另一个选择,它可以给你提供如下的控制器:

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

你可以试试 https://github.com/TiagoCardoso1983/association_observers。Rails 4还没有进行测试(Rails 4还没有发布) ,需要更多的合作,但是你可以检查一下它是否对你有用。

我的 Rails 3观察者的替代品是一个手动实现,它利用了模型中定义的回调,但是设法(正如 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 模型公开了两个事件:

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

一个警告是,所有模型中公开的所有事件的名称必须是唯一的。

我也有同样的问题! 我找到了一个解决方案 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

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

过去,人们都是“胖模特,瘦控制器”。然后,肥胖模型成为一个巨大的头痛,特别是在测试期间。最近推动瘦模型-这个想法是每个类应该处理一个责任和模型的工作是保存您的数据到数据库。那么,我所有复杂的业务逻辑都到哪里去了呢?在业务逻辑类中——表示事务的类。

当逻辑开始变得复杂时,这种方法可能会变成一个泥潭。但是这个概念是合理的——不是用回调或观察者隐式地触发难以测试和调试的事情,而是在类中显式地触发事情,该类将逻辑层置于模型之上。

看看 担忧

在模型目录中创建一个名为关注点的文件夹,并在其中添加一个模块:

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

这取决于你正在做什么,这可能会让你接近没有观察者。

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

在某些情况下,我只是使用 主动支持仪表

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

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

我建议不要在回调函数中添加太多逻辑,或者只是简单地移动代码来模拟观察者的行为,因为观察者模式已经有了合理的解决方案。

如果使用观察者是有意义的,那么尽一切可能使用观察者。只要明白,您将需要确保您的观察者逻辑遵循健全的编码实践,例如 SOLID。

观察者 gem 可以在 rubygems 上使用,如果你想把它添加回你的项目 Https://github.com/rails/rails-observers

看到这个简短的主线,虽然没有充分全面的讨论,我认为基本的论点是有效的。 Https://github.com/rails/rails-observers/issues/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

如果你喜欢这种方法,我推荐你读一读 布莱恩 · 赫尔姆坎普斯7种模式的博客文章。

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

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的错误报告