The Template Method: Revisiting Inheritance in Rails Controllers
In a previous post, I discussed the importance of adhering to SOLID design principles when grappling with inheritance in Rails. Specifically, I talked about how the flow of inheritance from the ApplicationController
to subordinate controllers can generate dangers of violating the Open/Closed and Liskov substitution principles.
We dealt with the Liskov substitution principle by refactoring this:
Into this:
The latter implementation conforms to Liskov because we’re no longer overwriting a method from a superclass in a subtype in a way that would break the superclass. But it also introduces a violation of Open/Closed - identify_user_in_params
would need to be modified each time we added a subtype. Accordingly, we further refactored to here:
This is a pretty alright stopping point, since we’re now delegating the work of identifying relevant parameters to the subtype. The ApplicationController
is open for extension, and closed for modification - new subtypes need to implement identify_user_in_params
, but don’t require changes in the ApplicationController
.
If you look closely at this last implementation, you may notice it looks a lot like an implementation of the template method design pattern. According to Russ Olsen,
In the Template Method pattern, the abstract base class controls the higher-level processing through the template method; the subclasses simply fill in the details.1
That passage aptly describes what’s going on here - the superclass implements shared behavior, and the behavior that varies is delegated to the subclasses. Only one thing is missing: the template!
Some might argue that there’s no need for a template in Ruby. After all, if we implement a new subclass and doesn’t either implement identify_user_in_params
or include skip_before_action :redirect_if_incorrect_user
, this code will throw an error: NameError: undefined local variable or method `identify_user_in_params` for #<...>
.
And, in fact, this very error introduces a deliberate violation of Liskov - no subtype is now substitutable for the superclass, since the ApplicationController
by itself cannot function (we’ll always get an error unless the subtype implements a workaround). Therefore, it’s best to be explicit about what’s going on. By introducing a template:
We make it obvious what we’re expecting from our subtypes.
-
Design Patterns in Ruby (Pearson Education: 2008), p. 65 ↩