Rails Observer Alternatives for 4.0

Ruby on-RailsRuby on-Rails-4

Ruby on-Rails Problem Overview


With Observers officially removed from Rails 4.0, I'm curious what other developers are using in their place. (Other than using the extracted gem.) While Observers were certainly abused and could easily become unwieldily at times, there were many use-cases outside of just cache-clearing where they were beneficial.

Take, for example, an application that needs to track changes to a model. An Observer could easily watch for changes on Model A and record those changes with Model B in the database. If you wanted to watch for changes across several models, then a single observer could handle that.

In Rails 4, I'm curious what strategies other developers are using in place of Observers to recreate that functionality.

Personally, I'm leaning towards a sort of "fat controller" implementation, where these changes are tracked in each models controller's create/update/delete method. While it bloats the behavior of each controller slightly, it does help in readability and understanding as all the code is in one place. The downside is that there's now code that is very similar scattered throughout several controllers. Extracting that code into helper methods is an option, but you're still left with calls to those methods littered everywhere. Not the end of the world, but not quite in the spirit of "skinny controllers" either.

ActiveRecord callbacks are another possible option, though one I don't personally like as it tends to couple two different models too closely together in my opinion.

So in the Rails 4, no-Observers world, if you had to create a new record after another record was created/updated/destroyed, what design pattern would you use? Fat controllers, ActiveRecord callbacks, or something else entirely?

Thank you.

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

Take a look at Concerns

Create a folder in your models directory called concerns. Add a module there:

module MyConcernModule
  extend ActiveSupport::Concern

  included do
    after_save :do_something
  end

  def do_something
     ...
  end
end

Next, include that in the models you wish to run the after_save in:

class MyModel < ActiveRecord::Base
  include MyConcernModule
end

Depending on what you're doing, this might get you close without observers.

Solution 2 - Ruby on-Rails

They are in a plugin now.

Can I also recommend an alternative which will give you controllers like:

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

Solution 3 - Ruby on-Rails

My suggestion is to read James Golick's blog post at http://jamesgolick.com/2010/3/14/crazy-heretical-and-awesome-the-way-i-write-rails-apps.html (try to ignore how immodest the title sounds).

Back in the day it was all "fat model, skinny controller". Then the fat models became a giant headache, especially during testing. More recently the push has been for skinny models -- the idea being that each class should be handling one responsibility and a model's job is to persist your data to a database. So where does all my complex business logic end up? In business logic classes -- classes that represent transactions.

This approach can turn into a quagmire (giggity) when the logic starts getting complicated. The concept is sound though -- instead of triggering things implicitly with callbacks or observers that are hard to test and debug, trigger things explicitly in a class that layers logic on top of your model.

Solution 4 - Ruby on-Rails

Using active record callbacks simply flips the dependency of your coupling. For instance, if you have modelA and a CacheObserver observing modelA rails 3 style, you can remove CacheObserver with no issue. Now, instead say A has to manually invoke the CacheObserver after save, which would be rails 4. You've simply moved your dependency so you can safely remove A but not CacheObserver.

Now, from my ivory tower I prefer the observer to be dependent on the model it's observing. Do I care enough to clutter up my controllers? For me, the answer is no.

Presumably you've put some thought into why you want/need the observer, and thus creating a model dependent upon its observer is not a terrible tragedy.

I also have a (reasonably grounded, I think) distaste for any sort of observer being dependent on a controller action. Suddenly you have to inject your observer in any controller action (or another model) that may update the model you want observed. If you can guarantee your app will only ever modify instances via create/update controller actions, more power to you, but that's not an assumption I would make about a rails application (consider nested forms, model business logic updating associations, etc.)

Solution 5 - Ruby on-Rails

Wisper is a great solution. My personal preference for callbacks is that they're fired by the models but the events are only listened to when a request comes in i.e. I don't want callbacks fired while I'm setting up models in tests etc. but I do want them fired whenever controllers are involved. This is really easy to setup with Wisper because you can tell it to only listen to events inside a block.

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

Solution 6 - Ruby on-Rails

In some cases I simply use 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

Solution 7 - Ruby on-Rails

My alternative to Rails 3 Observers is a manual implementation which utilizes a callback defined within the model yet manages to (as agmin states in his answer above) "flip the dependency...coupling".

My objects inherit from a base class which provides for registering observers:

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

(Granted, in the spirit of composition over inheritance, the above code could be placed in a module and mixed in each model.)

An initializer registers observers:

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

Each model can then define its own observable events, beyond the basic ActiveRecord callbacks. For instance, my User model exposes 2 events:

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

Any observer that wishes to receive notifications for those events merely needs to (1) register with the model that exposes the event and (2) have a method whose name matches the event. As one might expect, multiple observers can register for the same event, and (in reference to the 2nd paragraph of the original question) an observer can watch for events across several models.

The NotificationSender and ProfilePictureCreator observer classes below define methods for the events exposed by various models:

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

One caveat is that the names of all events exposed across all the models must be unique.

Solution 8 - Ruby on-Rails

I think the the issue with Observers being deprecated is not that observers were bad in and of themselves but that they were being abused.

I would caution against adding too much logic in your callbacks or simply moving code around to simulate the behavior of an observer when there is already a sound solution to this problem the Observer pattern.

If it makes sense to use observers then by all means use observers. Just understand that you will need to make sure that your observer logic follows sound coding practices for example SOLID.

The observer gem is available on rubygems if you want to add it back to your project https://github.com/rails/rails-observers

see this brief thread, while not full comprehensive discussion I think the basic argument is valid. https://github.com/rails/rails-observers/issues/2

Solution 9 - Ruby on-Rails

You could try https://github.com/TiagoCardoso1983/association_observers . It is not yet tested for rails 4 (which wasn't launched yet), and needs some more collaboration, but you can check if it does the trick for you.

Solution 10 - Ruby on-Rails

How about using a PORO instead?

The logic behind this is that your 'extra actions on save' are likely going to be business logic. This I like to keep separate from both AR models (which should be as simple as possible) and controllers (which are bothersome to test properly)

class LoggedUpdater

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

end

And simply call it as such:

LoggedUpdater.save!(user)

You could even expand on it, by injecting extra post-save action objects

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

And to give an example of the 'extras'. You might want to spiffy them up a bit though:

class EmailLogger
  def call(msg)
    #send email with msg
  end
end

If you like this approach, I recommend a read of Bryan Helmkamps 7 Patterns blog post.

EDIT: I should also mention that the above solution allows for adding transaction logic as well when needed. E.g. with ActiveRecord and a supported database:

class LoggedUpdater

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

end

Solution 11 - Ruby on-Rails

It's worth mentioning that Observable module from Ruby standard library cannot be used in active-record-like objects since instance methods changed? and changed will clash with the ones from ActiveModel::Dirty.

Bug report for Rails 2.3.2

Solution 12 - Ruby on-Rails

I have the same probjem! I find a solution ActiveModel::Dirty so you can track your model changes!

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionkennycView Question on Stackoverflow
Solution 1 - Ruby on-RailsUncleAdamView Answer on Stackoverflow
Solution 2 - Ruby on-RailsKrisView Answer on Stackoverflow
Solution 3 - Ruby on-RailsMikeJView Answer on Stackoverflow
Solution 4 - Ruby on-RailsagminView Answer on Stackoverflow
Solution 5 - Ruby on-RailsopsbView Answer on Stackoverflow
Solution 6 - Ruby on-RailsPanicView Answer on Stackoverflow
Solution 7 - Ruby on-RailsMark SchneiderView Answer on Stackoverflow
Solution 8 - Ruby on-RailshraynaudView Answer on Stackoverflow
Solution 9 - Ruby on-RailsChuckEView Answer on Stackoverflow
Solution 10 - Ruby on-RailsHouenView Answer on Stackoverflow
Solution 11 - Ruby on-RailsArturView Answer on Stackoverflow
Solution 12 - Ruby on-RailsmsrootView Answer on Stackoverflow