Last Updated: February 25, 2016
·
1.738K
· sslotsky

Observing record changes across multiple Rails engines with Wisper

In a recent project, I had my first opportunity to work with engines, which provides a great opportunity to modularize your Rails application. Our application was structured such that there existed a Common engine with models that could be accessed from all other engines. Our clients needed a variety of operations to be executed when changes were made to an Organization record, and these procedures were different enough in nature to warrant having different engines responsible for performing different sets of instructions.

We decided on a pub-sub package called Wisper to broadcast changes to multiple listeners. Wisper is simple to install and use, but we found our use case to be vastly different from their examples; they show a service being created in the controller, and having the controller code assign subscribers, but we have multiple locations in our codebase where an Organization might be modified, and don't want to repeat all this subscription code every time. We also don't want to have to remember the names of all the listeners, but instead have a convenient way to loop through them.

So what do we need to do?

1) Create a mechanism for adding a listener to the Common engine

# engines/common/lib/common.rb
require "common/engine"

module Common
  def self.organization_listeners
    @@organization_listeners ||= []
  end

  def self.add_organization_listener klass
    @@organization_listeners ||= []
    @@organization_listeners.push klass
  end
end

2) An Organization registers its subscribers on initialization and publishes to them when it saves.

# engines/common/app/models/common/organization.rb
module Common
  class Organization
    include ::Wisper::Publisher

    after_initialize :register_listeners
    after_save :publish_changed_attributes

    private

    def register_listeners
      Common.organization_listeners.each { |l| self.subscribe(l.new) }
    end

    def publish_changed_attributes
      publish(:organization_changed, self, changed_attributes.dup)
    end
  end
end

3) Define a listener in another engine

# engines/financials/lib/listeners/my_listener.rb
class MyListener
  def organization_changed(organization, changed_attrs)
    do_something(organization, changed_attrs)
  end
end

4) Register the listener

# engines/financials/config/initializers/configure_common.rb
require 'listeners/my_listener'

Common.add_organization_listener(MyListener)

And that's it. Here's an easy way to test that it's working:

# engines/financials/spec/models/my_listener.spec
require 'spec_helper'
require 'listeners/my_listener'

describe MyListener do
  context "when an organization is modified" do
    it "should be notified" do
      organization = FactoryGirl.create(:organization)
      MyListener.any_instance.
        should_receive(:organization_changed).
        with(organization, anything())
      organization.name = "foo bar baz"
      organization.save
    end
  end
end