Avoiding "Call Super" with Callbacks
Updated 2015-12-21. The example I originally used in this article was not very good, and apparently gave the false impression that I was encouraging the use of callbacks as a general replacement for
super. I’ve updated the example with something closer to my original use case.
Call Super is situation in which a superclass requires that subclasses invoke
super when overriding a method. Martin Fowler is, I believe, among the first to have labelled this an anti-pattern (“minor code smell”, in his words), and he’s careful to point out the problem is not with the
super call, but with the framework’s requirement that you must remember to call
super. (If a
super call is optional, it’s not a code smell.)
Here’s a concrete example of where “Call Super” is a problem. Let’s imagine we’ve got a library system that tracks grace periods and late fees. We have a “rules” class that tells us what the grace period for a particular book is, and what the corresponding late fee is.
We’ve got the base class set up with a default grace period and default late fee, and we’ve got a method where the concrete values for those settings are pulled out of the options hash.
Now, let’s say we have a special case when dealing with fiction, so we subclass our rules and override things. We want the subclass to have a different default grace period and late fee, and we want the late fee to double if the book has a waiting list.
The anti-pattern is manifested here because we must remember to call
super. If we don’t, some of our rules won’t get initialized.
So what to do instead? Fowler recommends the use of a Template Method, where the superclass defines a (possibly empty) method, and then calls it at the point of extension. Subclasses can then override that template method, if needed.
This works pretty well. I’ve been trying to use this pattern more recently, and I’m really happy with what it’s done to my code. However… it falls down a bit when you’re dealing with a more complex hierarchy of classes. For example, what happens when our rules change for a particular type of fiction? Let’s say the grace period is actually just a single day for vampire romance novels.
If we want to change a default for our
VampireRomanceRules objects, we have to override the template method, and remember to call super! So what do we do? Well, we could add a new template method for this second layer of inheritance:
But this quickly becomes a mess. The programmer using this framework has to keep track of which initialization method to implement based on what the superclass is, and that’s not a happy situation.
This is where callbacks have been really helpful for me. Specifically, ActiveSupport has a robust callback implementation that you can drop into any class. Then, your subclasses can define
initialize callbacks (or any others that you need), which the superclass will call before or after the appropriate extension points.
Every class can have it’s own unique initialization code, without worrying about overriding a superclass’ implementation, or even knowing what method is used to initialize the superclass. It’s super clean, very self-documenting, and easy to use, too.
The only wart, in my opinion, is how ugly that
set_callback invocation looks. In practice, I usually wrap that in a helper method on the superclass:
Lastly, when using
ActiveSupport::Callbacks, it’s good to understand a few things:
- Callbacks registered with
:beforeare called in reverse declaration order. That is to say, the last one registered is called first. Sometimes this is good, if you want a subclass to declare something that an earlier callback needs to consume. If you want a subclass to override something, though, don’t use
- Callbacks registered with
:afterare called in declaration order. The last one registered is called last. This is ideal when a subclass needs to override or alter something that an earlier callback has configured.
- You can call
run_callbackswithout a block, too. In this case, the
:beforecallbacks are invoked, and then the
:aftercallbacks are invoked immediately afterward.
Do you have any helpful tricks that you’ve used to avoid Call Super? Share them in the comments!