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
params
hash.
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.
Notably, 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_now
or 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 perform
method:
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.
Top comments (0)