If you search StackOverflow for “Rails callbacks”, a large number of the results pertain to seeking means to avoid issuing the callback in certain contexts. It almost seems as though Rails developers discover a need to avoid callbacks as soon as they discover their existence.
Normally, this would be a cause for concern, that perhaps the feature should be avoided altogether or even removed, but callbacks are still part of Rails. Maybe the problem goes deeper.
What is a Callback?
As you likely already know, callbacks are just hooks into an
object’s life cycle. Actions can be performed “before”, “after”, or even “around”
ActiveRecord events, such as
create. Also, callbacks
are cumulative, so you can have two actions which occur
those callbacks will be executed in the order they are occur.
Where Trouble Begins
Developers usually start noticing callback pain during testing. If you’re not
ActiveRecord models, you’ll begin noticing pain later as your
application grows and as more logic is required to call or avoid the callback.
I say, “developers usually start noticing callback pain during testing” because in order to speed up tests or to get them to pass, it becomes necessary to “stub out” the callback actions. If you don’t stub out the action, then you must add the supporting data structure, class, and/or logic to each test in order for it to pass.
Here’s an example of what I mean:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
In order to get that example code to pass,
notify_followers must be “stubbed
out”. If it isn’t, and if
followers are used within the mailer, the test will fail because it’s not able to execute the delivery (i.e. it’ll error out due to
What About Observers?
Rails developers who’ve begun moving into a more Object Oriented mindset might ask, “What about using observers instead of callbacks?” It’s the right direction: by creating an observer, you move responsibilities which don’t belong in the object being observed to the observer.
The problem is that observers in Rails are kind of like ninja callbacks: they perform the same function as callbacks, they just work in the shadows. Unless you look at the file system, you are very likely to forget Observers even exist in your application.
Furthermore, observers are assigned to their appropriate class when Rails starts up, and Rails starts up when you run your tests. Once again, you’ll start feeling pain in your tests first, because in order to avoid observer calls in your tests, you will need to either create all the dependent objects or install a gem such as no_peeping_toms. Just like callbacks, observers run every time their condition is met.
Aside: Herman Moreno wrote a good post on undocumented observer usage: Fun with ActiveRecord::Observer.
Why Are Callbacks So Problematic?
In his post on ActiveRecord, Caching, and the Single Responsibility Principle, Joshua Clayton noticed “after_* callbacks on Rails models seem to have some of the most tightly-coupled code, especially when it comes to models with associations.”
It’s no coincidence. “before_” callbacks are generally used to prepare an object to be saved. Updating timestamps or incrementing counters on the object are the sort of things we do “before” the object is saved. On the other hand, “after_*” callbacks are primarily used in relation to saving or persisting the object. Once the object is saved, the purpose (i.e. responsibility) of the object has been fulfilled, and so what we usually see are callbacks reaching outside of its area of responsibility, and that’s when we run into problems.
Solving the Problem
Jonathan Wallace, over at the Big Nerd Ranch, ran into to same problems and came up with one simple rule: “Use a callback only when the logic refers to state internal to the object.” (The only acceptable use for callbacks in Rails ever)
If we can’t use callbacks which extend responsibility outside their class, what do we do? We make an object whose responsibility is to handle that callback.
Let’s look at a hypothetical example. This is what we might originally have:
1 2 3 4 5 6 7 8 9 10 11 12 13
1 2 3 4 5 6 7 8 9
In the above example we can see that when an order is saved, it’s going to shoot
off an email to the customer. That Mailer is going to use the
order object to
retrieve the ordering
user and the products which were purchased and likely
use them in the email. Pretty simple, right?
In a test, however, any time an order is saved to the database,
products will need to be created, or the
method will need to be stubbed out –
Here’s what happens when we move some responsibilities:
Order model is much simpler.
1 2 3 4 5
Here’s our new class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
What we’ve done above is moved the process of saving the order and sending the
notification out of the
Order model and into a PORO (Plain Old Ruby Object)
class. This class is responsible for completing an order. Y’know, saving the
order and letting the customer know it worked.
By doing this, we no longer have to stub out the notification method in our tests, we’ve made it a simple matter to create an order without requiring an email to be sent, and we’re following good Object Oriented design by making sure our classes have a single responsibility (SRP).
It’s a simple matter to use this in our controller too:
1 2 3 4 5 6 7 8 9 10 11
As much as I complain about callbacks, they’re really not bad as long as you remember the rule: “Use a callback only when the logic refers to state internal to the object.” And really, that can be applied to any method.
When you start to feel those first twinges of pain from your tests, whether from callbacks or otherwise, consider if what you are trying to do exceeds your class’s responsibility. Creating a new class is a simple matter, especially compared to the pain and frustration caused by not doing it.
Many thanks to Pat Shaughnessy for proof-reading and providing feedback.