2013-03-01 49 views
142

随着Observers正式removed from Rails 4.0,我很好奇其他开发人员正在使用他们的地方。 (除了使用提取的宝石之外)虽然观察者当然受到了滥用,并且有时很容易变得笨拙,但除了缓存清理之外,还有许多用例是有益的。Rails Observer替代品4.0

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

在Rails 4,我很好奇,其他开发人员使用的地方观察什么样的战略来重建该功能。

就个人而言,我倾向于一种“脂肪控制器”的实施,在这些变化中的每个模型控制器的创建/更新/删除方法跟踪。虽然它稍微扩大了每个控制器的行为,但它确实有助于可读性和理解,因为所有代码都位于同一位置。缺点是现在有几个控制器中的代码非常相似。将代码提取到辅助方法中是一种选择,但您仍然会对那些遍地乱放的方法进行调用。不是世界的尽头,但也不完全符合“瘦身控制器”的精神。

ActiveRecord的回调是另一种可能的选择,虽然一个我不喜欢的个人,因为它在我看来,往往夫妻两个不同的模型太紧密地联系在一起。

在钢轨4

所以,没有观察世界,如果你要创建一个新的纪录再创历史新高后创建/更新/销毁,你会用什么设计模式?脂肪控制器,ActiveRecord回调,或其他完全吗?

谢谢。

+3

我真的很惊讶有没有张贴这个问题更多的答案。有点令人不安。 – courtsimas 2013-05-07 20:05:21

+18

https://github.com/krisleech/wisper – Kris 2013-05-17 14:36:26

回答

13

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

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

想必你已经花了心思,为什么你想/需要的观测,并因此形成取决于其观测模型是不是一个可怕的悲剧。

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

+1

感谢您的评论@agmin。如果有更好的设计模式,我很乐意使用Observer。我最感兴趣的是其他人如何构建他们的代码和依赖以提供类似的功能(不包括缓存)。就我而言,我希望在更新属性时随时记录对模型的更改。我曾经使用Observer来做到这一点。现在我试图在胖控制器,AR回调或其他我没有想到的东西之间做出决定。目前看起来并不高雅。 – kennyc 2013-03-02 01:43:32

32

现在他们在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 
+0

ActiveSupport :: Notifications怎么样? – svoop 2014-03-03 20:30:45

+0

@svoop'ActiveSupport :: Notifications'适用于检测,而不是通用的子/ pub。 – Kris 2014-03-05 09:58:24

+0

@Kris - 你是对的。它主要用于检测,但我不知道是什么阻止它被用作pub/sub的通用方法?它确实提供了基本的构建模块,对吧?换句话说,与'ActiveSupport :: Notifications'相比,wisper的优缺点是什么? – gingerlime 2014-08-05 06:37:23

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 callba中正。例如,我的用户模型公开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 

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

-2

我有同样的probjem!我找到了解决方案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

18

我的建议是阅读詹姆斯Golick的博客文章在http://jamesgolick.com/2010/3/14/crazy-heretical-and-awesome-the-way-i-write-rails-apps.html(试图忽略标题怎么听起来不庄重)。

早在一天,这一切都是“胖模型,瘦控制器”。然后,脂肪模型成为一个巨大的头痛,特别是在测试过程中。最近,推一直瘦车型 - 想法是,每个类应处理一个责任和模特的工作就是你的数据保存到数据库中。那么,我所有复杂的商业逻辑究竟在哪里结束呢?在业务逻辑类中 - 代表事务的类。

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

+3

在过去的几个月里,我一直在做这样的项目。你最终会得到很多小服务,但测试和维护的方便性绝对超过了这些缺点。我在这个中型系统上的相当全面的规格仍然只需要5秒钟就可以运行:) – 2014-02-03 08:43:45

+0

也被称为PORO(普通老式红宝石对象)或服务对象 – 2017-03-26 13:38:44

71

看看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 

取决于你在做什么,这可能让你关闭而不观察员。

+18

这种方法存在问题。值得注意的是,它不会清理你的模型; _include_将模块中的方法复制回您的班级。将类方法提取到模块中可能需要关注它们,但该类仍然臃肿。 – 2014-03-13 16:18:50

+12

标题是'Rails Observer Alternatives for 4.0'而不是'我如何最小化膨胀'。 担心的事情不是Steven的工作吗?不,建议'膨胀'是为什么这不起作用的原因,因为观察者的替代品不够好。你将不得不提出一个更好的建议来帮助社区或解释为什么担心不能替代观察员。希望你会陈述两个= D – UncleAdam 2014-03-13 20:19:33

+10

膨胀始终是一个问题。一个更好的选择是[wisper](https://github.com/krisleech/wisper),如果正确实施,它可以让你把问题提取出来,分离出与模型没有紧密耦合的类。这也使得它更容易单独测试 – 2014-03-28 02:03:41

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 
8

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

我认为观察员的问题被弃用是不是观察员和他们自己的,但他们是被滥用的药物为不良。

我会提醒您不要在回调中添加太多的逻辑,或者只是简单地移动代码来模拟观察者的行为,此时观察者模式已经有了完善的解决方案。

如果使用观察者有意义,那么通过一切手段使用观察者。只要理解你需要确保你的观察者逻辑遵循声音编码实践,例如SOLID。如果你想重新添加到您的项目 https://github.com/rails/rails-observers

看到这个简短的线程

观察员宝石可以用RubyGems的,而不是充分和全面的讨论,我觉得基本论点是有效的。 https://github.com/rails/rails-observers/issues/2

1

如何使用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