Promoting DRY with the Template Method Design Pattern
A practical overview by example over JS/TS
I recently started providing consulting services for a new client.
Looking over their express based gateway server codebase, I quickly noticed that all their Controller methods basically implement the same flow, to some extent.
It was very apparent that a lot of code is being repeated and that there is an obvious template to this.
We can do better!
In OOP, the Template Method is one of the Behavioral Design Patterns.
It is usually implemented as an abstract class, that defines the skeleton of an operation as a series of high-level steps.
These steps are implemented in the same class, usually as abstract methods. Concrete sub-classes are expected (actually required) to provide concrete implementations for them specific to the sub-class.
Another common approach is to implement the step methods as hook methods in the superclass with empty bodies.
Concrete sub-classes are then able (but not required to) provide overrides that allow for customizing the operation.
“The intent of the template method is to define the overall structure of the operation, while allowing subclasses to refine, or redefine, certain steps”
As you can see from the snippet above, some of the steps are always called, while others are optional (opt-in) and we had to accommodate for that.
Showcase
I’d like to go over our approach to implement this. We preferred functional approach over classes (abstract or otherwise) to break down the flow into reusable atoms to promote DRY principles.
First, we had to define a few types
RouteHandlerPayload
is used to describe the accumulated data of all template steps calls, and it is used by steps along the way.
RouteHandlerSteps
is used to describe the types of steps the template expects (or supports).
Note, that all steps are optional except the operation
step.
Why? Since calling the factory method without the operation
step is meaningless, it will have nothing to do…
Next, the routeHandlerFactory
method is a factory that returns a standard express middleware function of the shape (req, res): Promise
.
It is provided with the routeName and steps, and calls each step when appropriate in the flow and only if it was provided (opt-in, remember?)
We also went ahead and implemented a set of default step implementations, to facilitate easier usage and code reuse.
Now, all the controller has to do, is:
- Define the operation.
This will mostly be the service call, but it can be as simple or as complicated as needed. It can be an internal flow or multiple API calls and some complex manipulation or calculation. It has access to theRouteHandlerPayload
that should provide all it needs to complete the logic. - Define the steps.
This will mostly include the operation defined above (since it is required), and optionally one or more extra opt-in steps - Call the routeHandlerFactory.
It will return a function that complies with the standard express middleware signature that implements the template. - Provide the generated template function to the route definition
The template pattern is all about flows that can be broken down to atomic steps
This is a modular and DRY way to implement route controllers specifically, but the template pattern is all about flows that can be broken down to steps.
This pattern can be utilized whenever you detect that there is a flow that repeats itself and can be broken down to incremental, atomic steps.
Remember, that some of the steps might be optional opt-in to promote a wider range to usages
Also, think about providing a set of default step implementations alongside providing the ability for your users to override the defaults.
I hope you find this useful, please let me know if you have comments!