Last Updated: February 25, 2016
·
1.861K
· 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