It's not rare to see someone associating activerecord callbacks with something bad or wrong, avoiding at all cost using them and thinking in other ways to achieve the desired goal, although i believe that nothing is absolute and there are situations where they are needed.
Taking the following scenario as example
class Purchase < ApplicationRecord
belongs_to :user
def confirm!(user)
self.user = user
self.confirmed = true
PurchaseProcessor.create!(user: user)
Save!
end
end
We have the class Purchase
which have a methodconfirm!
Responsible for changing the state ofPurchase
, however this can lead us to a problem of consistency capable of creating a headache because the ActiveRecord automatically generates methods for the attributes and they can be changed in other fluxes
purchase.update_attributes!(confirmed: true)
Purchase.new(confirmed: true).save
Purchase.create!(confirmed: true)
This is a big problem having in mind that the state of the object is inconsistent, it is confirmed but dont have user and the processor wasnt executed
If exist a worker that generates any important report of purchases with base at a condition setted on PurchaseProcessor
, the chance to leave your base inconsistency is huge
One option would be using callbacks
class Purchase < ApplicationRecord
belongs_to :user
validates_presence_of :user_id, if: :confirmed?
after_save :execute_purchase_processor_on_confirm
private
def execute_purchase_processor_on_confirm
If confirmed? && confirmed_changed?
PurchaseProcessor.create!(user: user)
end
end
end
In that way, independently of how any attribute is setted in any flux we have the garantee of the integrity
But there are cases where callbacks became a problem by itself, eg:
class User < ApplicationRecord
after_create :send_confirmation_email
def send_confirmation_email
UserMailer.registration_confirmation(self).deliver
end
end
This way maybe its necessary to create an user anywhere without calling the send_confirmation_email
, and with this design you can just do it by using the private method create_without_callbacks
who also ignores the validations ( not a good option )
I think it worths the question, how frequently you will have to create an user withotu sending the confirmation email? maybe it deserves to be splited in other class, eg:
class UserRegistration
def initialize(user)
@user = user
end
def register
@user.save
UserMailer.registration_confirmation(self).deliver
end
end
Maybe its better to split behaviors that is not expected by default with the goal to simplify the use and manteining ( you expect that creating a User
just create it without executing other actions or fluxes )
Callbacks are not all evil, but i believe that should had some reflection before using it, thinking about the context and the scenario... but as as rule i particularly like to start thinking about the design without the use of the callbacks and analyse if it can cause some inconsistency or indesired scenario
Top comments (1)
In the callback example, the downside is that now in tests you're not able to update the
confirmed
column without triggering the purchase processor.