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