During the last years Rails developers have been looking for ways to move from monolithic to component-based applications. One of these is the use of Rails Engines. The basic idea is to move away from the default MVC Rails architecture where “fat Models” end up being morbid obese.
If you are interested to know more about Rails Engines I highly recommend you to watch these two talks by Stephan Hagemann at Mountain West RubyConf 2013 and RailsConf 2014.
In a few words we’re talking about code that doesn’t pertain to your application core business logic but lives around it. Since we recognize that they’re not part of the core we can extract them into Rails Engines.
A typical example can be to extract email notifications functionality from the core of the application and move it to an Engine.
Decoupling engines
The most exciting thing, of course, is not just moving code around. It allows you to create APIs that developers can rely upon to build functionality. By encapsulating a feature to its own engine we can debug better since we know where all the code related to certain functionality is.
Using observers
One way to react to the changes of our models in core is to use good old observers. Before we had `ActiveModel::Observer` for that! And we still have it of course. The only issue now is that our observer class name is something like `EmailNotificator::UserObserver` and Rails cannot do the magic of inferring the Model from the class name. That’s when the convenient `observe` method comes to help. Look at the code to better understand how it works.
# engines/email_notificator/app/observers/email_notificator/user_observer.rb
module EmailNotificator
class UserObserver < ActiveModel::Observer
observe :user
end
end
In this Observer we're subscribing to core `User` model changes. If we need to observe engine specific Models we can do the following instead:
# engines/email_notificator/app/observers/email_notificator/person_observer.rb
module EmailNotificator
class UserObserver < ActiveModel::Observer
observe 'EmailNotificator::Person'
end
end
Custom events
When our engine is waiting for a custom event to happen outside the engine itself, for instance waiting for an external service webhook, we need a way to subscribe to it. We can apply the pub/sub pattern using ActiveSupport::Notification. The idea is to trigger an `ActiveSupport::Notification` event from the core (or another engine) and then let our engine listen to it:
In the case of webhooks we'll probably have a Controller inside an Engine that manages the incoming webhook. We can trigger the event inside the Controller's action:
# engines/webhook_manager/app/controllers/webhook_manager/webhook_controller.rb
# ...
def create
# code to store the webhook
ActiveSupport::Notifications.instrument('webhook.received', options: { extra: 'information' })
end
# ...
Then inside our engine code:
# engines/email_notificator/config/initializers/email_notificator.rb
ActiveSupport::Notifications.subscribe('webhook.received') do |name, start, finish, id, payload|
# do some stuff here
puts "A webwook has been received with some more extra #{payload[:options][:extra]}"
end
Now every time a `webhook.received` event is triggered our engine will execute the code in the block.
Conclusions
Thinking about components that communicate through events can help during the process of breaking monolithic apps into smaller components. It will make your code cleaner and easier to debug (`git grep` friendly). Give it a try!
