Rails Observer Alternatives for 4.0

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.


Solution 1:

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:

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:

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:

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.)