In a recent rails project I’ve been using a gem called wisper that provides publish-subscribe capabilities to ruby objects. I’ve been very happy with how it de-cluttered my callbacks, tidied up my models and moved it all into background jobs.
As your application grows you often find you models growing along with it. I’m a big fan of keeping my controllers skinny and pushing the logic into the model layer. But, over time, the models get bigger and become entangled with logic that is less and less relevant to the object itself. Updates to a model can trigger a chain of callbacks which can slow requests requests down as you wait for other changes to be carried out.
One way to address these issues is to replace some callbacks and use a publish/subscribe pattern instead.
Let’s look an example
Post model that has two on-create callbacks; one to notify editors a new post was created, and one to generate a feed item to add the new post to an activity feed.
class Post < ActiveRecord::Base after_commit :notify_editors, on: :create after_commit :generate_feed_item, on: :create private def notify_editors EditorMailer.send_notification(self).deliver_later end def generate_feed_item FeedItem.create(self) end end
Post objects should not have to know anything about editors or an activity feed. These callbacks are polluting the model - the editor and activity feed logic does not belong in the Post class. It’s also slowing our request down as we have to wait for the callbacks to complete before the request can return.
Adding some basic Pub/Sub
Instead of using callbacks, we can use a pub/sub pattern to publish model events. We then listen to these events elsewhere, carrying out actions whatever actions need to be done as a result. This decouples the unrelated logic, pulling it out of the model into a listener where relevant actions can be taken.
# app/models/post.rb class Post < ActiveRecord::Base after_commit :publish_notification, on: :create private def publish_notification ActiveSupport::Notifications.instrument('post.created', post: self) end end
# config/initializers/subscribers.rb ActiveSupport::Notifications.subscribe 'post.created' do |name, start, finish, id, payload| post = payload[:post] EditorMailer.send_notification(post).deliver_later FeedItem.create(post) end
Using ActiveSupport Instrumentation we can rewrite the above example. We replace our two callbacks with one, which just publishes a notification to broadcast that the post was created. Then a listener, subscribed to these notifications, looks after sending the editor notifications and creating the feed item.
With this setup, the
Post model, has no knowledge of editors or feed items and the listener just needs to know that when a new post is created, it send an editor_notification and creates a feed item - a much cleaner approach, adhering much closer to the single responsibility principle.
Active Support Instrumentation is built right it Active Support and is therefore already available in your application if you’re using Active Support. However, as the name suggests, it is geared towards instrumenting your code rather than decoupling your callbacks.
Wisper is an excellent gem by Kris Leech, that creates a very nice DSL for publishing and subscribing to notifications from your ruby models. I use this in conjunction with wisper-activerecord, another gem produced by Kris, to transparently publish life-cycle events from your ActiveRecord models.
Rewriting the above example with wisper, and wisper-active recored, removes most of the boilerplate.
# Gemfile gem 'wisper' gem 'wisper-activerecord'
# config/initializers/subscribers.rb Post.subscribe(PostSubscriber)
class Post < ActiveRecord::Base include Wisper.model end
class PostSubscriber def self.after_create(post) EditorMailer.send_notification(post).deliver_later FeedItem.create(post) end end
Using wisper, we’ve cleaned up the code and now have a clear separation of concerns. The
Post model can go about it’s business, publishing when it’s created/updated/destroyed, and the
PostSubscriber can listen the post events it wants. In this case, listening to the
Move it into the background
What’s more, is that this setup makes it even easier to move all this logic into the background. [wisper-activejob] (https://github.com/krisleech/wisper-activejob) let’s us leverage the power of active-job and carry out the Subscriber actions with sidekiq, delayed_job or whatever tool we’re using for background job processing.
The only thing we need to do is add the wisper-activejob gem:
# Gemfile gem 'wisper' gem 'wisper-activerecord' # at the time of publishing the version of wiper-activejob in rubygems.org as quite old gem 'wisper-activejob', github: 'krisleech/wisper-activejob'
And change the subscriber to listen asynchronously:
# config/initializers/subscribers.rb Post.subscribe(PostSubscriber, async: true)
Now when our request comes in to create a post, the
after_create notification is published and the request returns. The background processor looks after listening to the notifications and carrying out any actions for events it’s listening to.
Overall I feel we have gained a lot from these changes.
- it trims down our growing models
- helps avoid ugly chain of callbacks
- prevents polluting models with unrelated code
- moves our callbacks into the background, leaving our application free to receive more requests
There are may different ways to implement the pub/sub paradigm in ruby but I found wisper does a great job of removing boilerplate and providing a nice clean DSL.