Enable users to build up on your Rails Engines and Gems without monkey patching, inheritence or overriding methods
This short tutorial is intended for authors of Gems and Engines.
When writing gems or Rails engines they can be difficult for the user to extend.
For example Devise, the authentication Rails engine, has a Wiki page showing how to send an email after a user changes their password.
The Wiki suggests monkey patching your model overriding the
def update_with_password(params, *options) if super UserMailer.password_changed(self.id).deliver end end
Monkey patching is dangerous particually if the method is not part of the public API.
Even patch version updates of Devise could potentially break your code.
In addition the method is not very readable, it seems
super returns a boolean, but what is it actually doing? Can we be sure it will always return a boolean, or have the same method signature…
An alternative would be for Devise to broadcast events which the host application can listen for and react to. This can be achived with the Wisper gem.
In the Devise implementation of
update_with_password they could broadcast an event:
module DatabaseAuthenticatable extend ActiveSupport::Concern included do include Wisper.publisher # <-- LINE ADDED HERE end def update_with_password(params, *options) current_password = params.delete(:current_password) if params[:password].blank? params.delete(:password) params.delete(:password_confirmation) if params[:password_confirmation].blank? end result = if valid_password?(current_password) update_attributes(params, *options) broadcast(:password_updated, self) # <-- LINE ADDED HERE else self.assign_attributes(params, *options) self.valid? self.errors.add(:current_password, current_password.blank? ? :blank : :invalid) false end clean_up_passwords result end end
Within our Rails application we could then subscribe a listener for this event in an initializer:
User.subscribe(UserListener.new, prefix: 'on')
In the case of sending an email after the email address has changed the listener would look like this:
class UserListener def on_password_changed(user_id) UserMailer.password_changed(user_id).deliver end end
The nice thing about this pattern is the Gem/Engine author can broadcast many events, with a single line of code for each and does not need to consider the method name and signature since it no longer needs to be overridden in the Rails app.
On the flip side the Rails application does not need to know about the Gem internals and can be confident changes to the API will not break it.
- Easy for Engine/Gem author to impliment, simply add Wisper as a runtime dependency and use the broadcast method
- Easy for the Engine/Gem user, simply subscribe a listener, no need to monkey patch internals.
On top of this the additional options built in to Wisper are avalible including support for async events, note that the Gem/Engine author does not need to make any changes or add any additional dependencies to allow the Rails app to respond to its events async.
Other places Devise could broadcast events would be signin / signout / change password etc. A listener could subscribe to these events to, for example, record an audit of all logins.
In the above example I subscribed the listener to all instances of
User, but you could also subscribe to indavidual instances:
@user.subscribe(SpecialListener.new, prefix: 'on')
Any gem or engine can use this technique to provide optional, decoupled callbacks. I hope you consider it for your own Gem or Engine.