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.
Like this:
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
:before
are 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 usebefore
callbacks. - Callbacks registered with
:after
are 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_callbacks
without a block, too. In this case, the:before
callbacks are invoked, and then the:after
callbacks are invoked immediately afterward.
Do you have any helpful tricks that you’ve used to avoid Call Super? Share them in the comments!