Rails Observer替代品4.0

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

举个例子,一个需要跟踪模型变化的应用程序。 观察者可以很容易地观察模型A上的变化,并用数据库中的模型Blogging这些变化。 如果你想看几个模型的变化,那么一个观察者就可以处理这个变化。

在Rails 4中,我很好奇其他开发人员用什么策略代替观察者来重新创build这个function。

就我个人而言,我倾向于一种“胖控制器”的实现,在每个模型控制器的创build/更新/删除方法跟踪这些变化。 虽然它稍微膨胀了每个控制器的行为,但它确实有助于可读性和理解,因为所有的代码都在一个地方。 缺点是现在有几个控制器中的代码非常相似。 将代码提取到辅助方法中是一种select,但是仍然对那些遍地乱放的方法进行调用。 不是世界的尽头,但也不完全是“骨感的控制者”的精神。

ActiveRecordcallback是另一种可能的select,虽然我个人不喜欢,因为它往往将两个不同的模型紧密地结合在一起,在我看来。

所以在Rails 4中,没有观察者的世界,如果在创build/更新/销毁另一个logging之后必须创build新logging,那么将使用什么devise模式? 脂肪控制器,ActiveRecordcallback,或者其他的东西?

谢谢。

看看关注

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

module MyConcernModule extend ActiveSupport::Concern included do after_save :do_something end def do_something ... end end 

接下来,在你希望运行after_save的模型中join:

 class MyModel < ActiveRecord::Base include MyConcernModule end 

根据你在做什么,这可能会让你closures没有观察员。

他们现在在一个插件 。

我也可以推荐一个替代scheme ,它可以给你如下的控制器:

 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 

我的build议是阅读詹姆斯·戈利克的博客文章http://jamesgolick.com/2010/3/14/crazy-here-and-awesome-the-way-i-write-rails-apps.html (尝试忽略不正确的标题听起来)。

早在这一天,它就是“胖胖的模特,瘦瘦的控制者”。 然后胖子模型成为一个巨大的头痛,特别是在testing过程中。 最近的推动一直是为瘦身模式 – 这个想法是,每个类应该处理一个责任,模型的工作是将您的数据保存到数据库。 那么,我所有复杂的业务逻辑到底在哪里呢? 在业务逻辑类中 – 代表事务的类。

当逻辑开始变得复杂时,这种方法可能变成一个泥潭(giggity)。 但是这个概念是有道理的 – 不是用难以testing和debugging的callback函数或观察者来隐式地触发事物,而是在模型顶层的逻辑类中明确触发事物。

使用活动loggingcallback简单地翻转你的耦合依赖。 例如,如果您具有modelACacheObserver观察modelA rails 3样式,则可以CacheObserver移除CacheObserver 。 现在,相反, A必须在保存后手动调用CacheObserver ,这将是rails 4.您只需移动依赖项,以便可以安全地删除A而不是CacheObserver

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

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

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

Wisper是一个很好的解决scheme。 我个人对callback的偏好是它们被模型所激发,但事件只是在请求进来时才被听到,也就是说我不希望在testing中设置模型时触发callback,但是我希望它们无论何时涉及控制者都会被解雇 这与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 

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

我对Rails 3 Observers的替代是一个手动实现,它利用模型中定义的callback函数,然后设法(如上面的答案中的agmin状态)“翻转依赖…耦合”。

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

 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 

(当然,本着构造与inheritance的精神,上面的代码可以放在一个模块中,并在每个模型中混合使用。)

初始化程序注册观察者:

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

然后,每个模型可以定义自己的可观察事件,超出了基本的ActiveRecordcallback。 例如,我的用户模型公开了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 

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

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

我会提醒你不要在callback中添加太多的逻辑,或者只是简单地移动代码来模拟观察者的行为,当已经有一个合理的解决scheme观察者模式。

如果使用观察者有意义,那么通过一切手段使用观察者。 只要明白,你将需要确保你的观察员逻辑遵循健全的编码实践,例如SOLID。

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

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

你可以试试https://github.com/TiagoCardoso1983/association_observers 。 它还没有testing轨道4(尚未推出),需要一些更多的协作,但你可以检查它是否为你做的伎俩。

如何使用PORO?

这背后的逻辑是,你的'额外的行动'保存'可能会成为业务逻辑。 我喜欢与AR模型(​​应该尽可能简单)和控制器(它们很难正确testing)保持分离,

 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博客文章。

编辑:我还应该提到,上述解决scheme允许添加事务逻辑以及需要时。 例如与ActiveRecord和支持的数据库:

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

值得一提的是,由于实例方法的changed? ,Ruby标准库中的Observable模块不能用于Active-Record-like对象changed?changed将冲突与ActiveModel::Dirty

Bug报告为Rails 2.3.2

我有同样的问题! 我find一个解决schemeActiveModel ::脏,所以你可以跟踪你的模型的变化!

 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