Matt Swanson wrote an insightful post today on how to level-up your usage of Mailers in your Rails application. In it, he discussed how using "parameterized mailers" unlocked the ability to build mailers that could be sent from either a custom domain or the applications default domain. As he details in his post, a parameterized mailer is initialized with
params before calling the mailer method. So instead of:
NotificationMailer .comment_reply(user, comment) .deliver_later
You would write:
NotificationMailer .with(user: user, comment: comment) .comment_reply .deliver_later
I tweeted a quick response to Matt noting that parameterizing jobs can similarly unlock some powerful and useful functionality. Matt reminded me that a fuller post would be more helpful. So, here we are.
Well, what is "parameterizing" something like a mailer? While the implementation in Rails is somewhat more complicated for mailers, in essence
with is simply an alias for
new. That is, to parameterize is to initialize with state. Instead of passing all of the necessary information into the instance method, we pass it into the initialization method.
As Matt says in his post, this can feel like a difference without a point:
My first impression was that I didn’t quite understand the point of this. I generally prefer having the explicit method arguments on the mailer method compared to a generic
But, of course, there is a point. The point is that by moving the required state into a class instance, we make it possible to extend, prepend, and inject behavior that alters how the executing method behaves. In Matt's example, he used a before callback to inject behavior that switches the
from field for the address, depending on whether the
Account has a custom domain set or not.
ActiveJob is "parameterized" by default. As you recall, you define the executing method via
def perform in your job class; that is, you define an instance method named
perform. But, you invoke the job by using class methods—either
perform_later. Under the hood, ActiveJob's class methods are little more than short-hand for initializing the job class with the passed parameters and invoking the
def self.perform_now(...) job = new(...) job.perform(*job.arguments) end
This isn't exactly what the source code for
perform_now looks like, but the essence is the same. And what that means is that before your
perform method is invoked, your job has all of the information it needs for this particular execution. This is what allows callbacks to be useful, for example.
In my particular case, this has proven so powerful because it allows something like
AcidicJob to exist.
AcidicJob is a gem that provides a suite of features that you can use to make your jobs both more coherent and more resilient. And, at its heart, it functions by serializing and deserializing your job execution into a database record, and then leaning on the ACID guarantees provided by database engines for transactions. This is easy to do with ActiveJob, since the job instances are parameterized and thus can be serialized before the job is executed with the full set of information needed to execute that job.
At present, such behavior is not possible with pure Sidekiq because Sidekiq does not initialize a worker/job instance with the parameters needed to execute the worker/job. I started a conversation on why this would be valuable, but we haven't yet found an API that works well in the context of Sidekiq. This isn't to say anything negative about Sidekiq, which is fantastic software, but simply to point out what kind of flexibility is unlocked when parameterizing operations like jobs and mailers.
Matt's example is only one of many, many possible ways to make your mailers or jobs more flexible, coherent, and perhaps even resilient by leaning on the power of parameterized classes.