Four Alternatives to Using ActiveRecord Callbacks and Observers20 Sep 2015
Callbacks suck, and many experienced Rails programmers will tell you so. It took me a couple of years to finally understand why, but I did eventually, and I have rarely used them since.
If we should avoid callbacks, what should we use instead?
First, let’s look at a naive scenario. You have probably seen a similar example before.
A user registers and he or she is sent a welcome e-mail. We know that in most cases this is a bad1 idea. So how can we make this better?
1. Send the e-mail from the controller
The first and most obvious way is to send the e-mail from the controller.
This is better because our code is explicit and has no unexpected side effects. Calling
@user.save saves the user instance, and that’s it. Instead, the e-mail is sent explicitly.
In many cases this is fine, but there is a downside, and that is our code is not reusable. We might have to send an e-mail after user registration in other places (for example, from an admin panel or through an API call), and hardcoding this logic in the controller means we have to repeat ourselves.
2. Use a controller concern
Concerns are just Ruby modules we can use to share common functionality between different models and controllers. We can extract the bit of code we want to reuse and encapsulate it in a method in our module.
It’s not necessary to pass the user as an argument; methods in concerns have access to the state of the objects they’re mixed into. But I prefer to be more explicit than rely on state. Passing the user as an argument defines an explicit contract between the caller and the callee.
Here’s how we might use it.
Concerns give us code reuse, but at the cost of increased abstraction. For example, you can’t immediately see what
save_user_and_deliver_email does, and to find out you must open another file. It is also not immediately obvious where else this method is being used.
3. Use a service object
Service objects encapsulate bits of reusable functionality in their own class. Because they’re not modules, they don’t bloat other classes or suffer from the same issues that concerns do. They are easy to reason about, and a joy to test.
We can reuse this service object from anywhere–the console, a rake task, or another controller–free from side effects.
I find service objects are easy to extend. Let’s say that, besides sending an e-mail, we need to create an activity item for a feed. This is an easy change to make.
4. Use a model method
If you don’t find the benefits of using concerns and service objects compelling, you can replace callbacks with vanilla methods in your model.
I’m not a fan of this approach, even if it’s an improvement on callbacks. In my opinion, an ActiveRecord model should be responsible for its persistence and internal business logic only. It should not concern itself with sending e-mails.
When do I use callbacks?
I use callbacks when dealing with the internal state of the object.
Callbacks are not intrinsically bad, and they have their uses. But they give you a lot of rope to tie yourself with. They are attractive because they make certain tasks look and feel deceptively easy. It’s always worth asking yourself if there’s a better way.
Because it violates SRP. It tightly couples user creation with sending emails. It obfuscates the intention of your code. It leads to undesirable side effects. It makes your code deterministic. Etc… ↩
We can check for the presence of
value, but we shouldn’t;
valuewill be an empty string when a form is submitted with a blank email. If we get errors because of
nil, it’s probably a bug in the application, and we want it to fail. ↩