Improve up your Rails controllers using a pub/sub pattern
TL;DR — This article will introduce you to the pub/sub pattern, and show you how to improve your Rails controllers by implementing it using the whisper gem.
Ruby on Rails is an amazing and mature framework, which many of us instinctively turn into whenever we need to get something simple up and running as soon as possible. However, many will also agree that Rails simplicity is also its biggest flaw. As your application grows, your sleek-and-fast Rails app suddenly become messy and disorganized. I like the MVC pattern, but when the number of dependencies between different entities grows, you need something more than that.
Publisher & Subscriber
One approach I really wanted to try out for some time was the pub/sub pattern. In this pattern, some objects publish special messages (with an optional payload), and other subscribed objects are listening to them. Whenever a subscribed object hears a message it is interested in, it can do something with it. whisper gem adds pub/sub capabilities to Ruby objects, so why not try using that in our Rails app.
Let’s take a look at this hypothetical Rails controller which does the following:
- When a
Post
is created, it sends an email notification to its author - If the post has 300 words or more, it creates a
FeaturePost
object too.
This simple implementation looks OK, but it violates one common principle — the Single Responsibility Principle (SRP). Why? Well, our PostsController
is now responsible not only for creating posts, but also sending emails and creating other objects (FeaturedPost
). And if you’re planning to write a test for that controller, you should probably test all those additional things too. PostsController
sending emails? This ain’t good.
BTW, for the rest of the article, I’m going to skip the def post_params
part of the controller, in order to save you time reading.
So let’s try to implement a service object that will create a Post
, and nothing else — and then, depending on the result of that creation, it’s going to publish some sort of Success
or Failure
message.
Ok, so if post.save
returns true, we broadcast post_created
string, together with the Post
object we just created — but if post.save
returns false, we broadcast post_not_created
string. Sounds easy so far. Let’s hook it up into our PostsController
This looks promising, but we’re missing something important here. Every Rails controller should have some sort of redirect_to
or render
, right? Let’s use the messages that Services::Post::Create
broadcasts, and let’s add that back to our controller. We’re going to use the on
method that comes from whisper.
Better, but there are still two pieces missing. Our original controller was also sending email notification and creating aFeaturedPost
object, and this controller does not. And we don’t want to put that inside that controller, because this is exactly what we’re trying to avoid.
But what if we could tell someone to listen for that post_created
message, and then, when that message is broadcasted, send an email notification, and create a FeaturedPost
?
pub/sub subscriptions to the rescue. Let’s create two listeners objects.
Ok, so now we have two objects. Listeners::Mailer
will be responsible for sending an email notification to the author of that post, and Listeners::FeaturedPost
will be responsible for creating a FeaturedPost
but only when the post has more than 300 words.
Note how the methods defined inside those listeners are named exactly the same as the message string that Services::Post::Create
broadcasts. This is very important because this connects a Listener
with our Service
Ok, let’s add those Listeners
to our controller.
So what we did here is we’ve subscribed both listeners (using a subscribe
method) to ourServices::Post::Create
service object, so that any message broadcasted by Services::Post::Create
will be passed down to those listeners. And if that message happens to be post_created
, then both Listeners::Mailer#post_created
and Listeners::FeaturedPost#post_created
methods are going to be executed.
Voilà 🎉 Our app now does the same thing as it did at the beginning of this article.
I really like using this pattern because it’s really simple to start with and it really helps you spread responsibilities across many smaller objects.
If you look at the PostsController
now, you will notice it’s responsible for only one thing. One could think it's a creation of aPost
, but that’s not true. If you look closely, you will notice that PostsController
does not create Post
objects per se — instead, it runs some service, nothing else. So in practice, the controller becomes a layer that converts incoming HTTP requests into a call
method call on some service class. This drastically simplifies writing tests.
My Services::Post::Create
is a small & compact object which does one thing only — it tries to persist w post in a database and broadcasts a proper message depending on the result. Same with bothListeners
— each one is responsible for one small tiny piece of our business logic, and nothing else. This makes those object really easy to test too.
Want more? Here are a few recommendations
There are a few things you might want to consider when using this pattern.
- Use Ruby constants to store messages
Currently, the post_created
message string must match the method name inside your listeners' classes. If you make a typo in one of the places, it might be hard to catch what’s wrong. That’s why I recommend using constants to store those string.
Then, make sure you publish them through your service class.
Next, make sure your controller uses them too.
And last but not least — use them to define methods inside your listeners
That way, if you make a typo in a constant name, your app will immediately throw a NameError: uninitialized constant
error at you :)
2. Don’t be afraid to use other service objects inside your listeners
In our implementation, Listeners::Mailer
is actually responsible for sending emails, and Listeners::FeaturedPost
is responsible for creating FeaturedPosts
. I think it’s OK, but I think it would be even more kosher to maybe have a separate service object to do just that, and use listeners as a layer that only turns incoming pub/sub messages into other services.
This is how this could look like
I really hope some of you will find this pattern interesting, and you will give it a try. I highly recommend it. If you have any questions, please let me know in the comments below.