The Template-Method design pattern is relatively common and frequently used by third-party frameworks or libraries. The name may look confusing, especially for C++ developers, but the template in the name has nothing to do with C++ function templates. Instead, the Template-Method is a design pattern that solves two challenges programmers face often:
1. Code Reuse Imagine you have two methods that almost do the same, but there are slight parts where the implementation differs. You can use the Template-Method to handle both cases while reusing most of your existing code.
2. Customize single parts of a larger algorithm You have an algorithm consisting of several steps executed in a specific order. While users of your code shouldn't edit the algorithm as a whole, you want to allow them to change individual steps without bothering with the whole algorithm.
Okay, at first view, these two cases may look a bit abstract - especially the second one - so let's look at an example first:
A simple example: Ordering Coffee
Let's start with a real-life "problem" - Ordering a coffee in a Coffee Shop.
No matter which coffee shop you enter, the process is nearly almost the same:
- You choose your beverage.
- You pay for it.
- The barista prepares it for you.
- And at the end, you receive your drink.
The order in which these steps appear stays mostly the same, no matter whether you go to a physical shop at the corner or order your coffee through an online delivery website. However, how these steps are actually executed can very well depend on the kind of shop you've chosen.
In an online shop, you navigate through a list of products and select the one you like, pay via an online payment system like Paypal, and wait till your beverage gets delivered to your door.
In contrast, in a physical shop, you tell the barista what type of coffee you like, pay with cash, credit card, or phone, wait till your coffee gets prepared, and grep it at the counter.
Let's now say we must model this scenario with an OOP language. We could create one class per coffee shop and implement each process separately. The result would then look something like this:
While this does the job, there is room for improvement: The general structure of our algorithm orderCoffee
is always the same, yet we had to implement it twice.
This gets worse if we add a third or fourth type of coffee shop to our system - we would have to reimplement or copy/paste the method every time!
Also note the step barista prepares coffee: It will most likely be the same in both shops. However, with our approach, we would have to implement it for every type of coffee shop again. Not the best solutions, especially if we want to change something in the future...
Let's see how the Template-Method can help us here.
The Template-Method Pattern
To use this pattern, we have to do three things.
First, we define an abstract class that works as a placeholder for all kinds of coffee shops. That's definitely the easy task.
Secondly, we must split our algorithm into individual steps called primitive operations. These operations represent the variable parts of our algorithm.
Actually, this sounds easier than it often is in reality: To do this properly, we must already have an idea of the different use cases our application must serve and how those may differ in their details. And trust me, there is always one case you never thought of...
Luckily, we have already decomposed our workflow for our coffee shop example. All we have to do is add one method per step to our abstract class. Don't worry about the implementation - details will follow.
Third and last step: We add the orderCoffee
method to our class, as we already did in our example.
This method contains the logic of our algorithm, i.e., it calls the primitive operations we defined earlier.
And that's already our design pattern!
The orderCoffee
method is also called a template method, because it works like a drawing template that contains empty shapes one can fill out using a crayon.
We fill those shapes by subclassing our abstract class and providing implementations for each primitive operation.
Since, in our example, we had two different types of coffee shops, we would also need two subclasses:
Please also note that our diagram did not provide implementations for the baristaPreparesBeverage()
. The method has the same implementation for both types of coffee shops. Hence, it's enough to implement it in the base class and let the subclasses reuse it.
The following diagram is a more generalized version of the pattern. Note the abstract class containing the templateMethod()
and the primitive operations representing the individual steps. The Concrete subclasses can choose which primitive operation they want to implement.
Since we know have our pattern's architectural structure, let's look at some details regarding its implementation.
Implementation Details
Abstract methods or default implementation?
When designing the abstract base class, you must decide whether to provide default implementations for the primitive operations or keep them abstract. Having a default implementation allows for easier code reuse, but you must also specify whether the base implementations are optional or must be called when overriding the methods before any custom logic.
Also, don't forget that having several dozens of abstract primitive operations your users must implement when deriving your class can be daunting, so always try to keep the number of abstract primitive operations as low as possible.
Making the template method overrideable
The template method defines the invariant structure of your algorithm that shouldn't change for derived classes.
However, there are always exceptions.
Consider our Coffee Shop example:
Our algorithm assumes customers always pay before receiving their beverage.
But what if we get a third type of coffee shop - maybe a traditional Viennese coffee house - where it is common to pay after your coffee is served? In that case, the derived class must alter the oderCoffee
method to change the execution order of the steps.
Whether you want to allow such behavior depends on how complex your template method's implementation is and how easy it would be for your users to reimplement and alter that logic.
Hooks
These are special primitive operations that are not abstract but neither don't contain any implementation. Those methods provide extension points where derived classes can hook into the workflow and add their custom implementation.
public void templateMethod() {
// hook called before operation
beforePrimitiveOperation1();
primitiveOperation1();
// hook called after operation
afterPrimitiveOpeation1();
}
The beforePrimitiveOperation1
and afterPrimitiveOperation2
are primitive operations derived classes can implement to add additional logic before and after a primitive operation is called.
UI frameworks often use this approach to let users add their custom code to the rendering workflow.
Take, for instance, the React library that provides several hooks users can implement to handle various component lifecycle events like componentDidMount
or componentWillUnmount
to add custom initialization or cleanup code.
Some things to consider
Inversion of Control
The pattern changes the control flow in your application. Instead of calling your primitive operations directly, you let the template method decide when and in which context your operations are invoked.
But this also means you can only access the data you retrieve as an input parameter from the template method. If you need more information than provided, your only option is to reimplement the template method, which can require some work.
The same applies if you need to transfer data between your steps. In that case, you must add members to your class and store the information there. In the following diagram, primitiveOperation1
stores data in the object's state, and primitiveOperation2
queries this information again.
While this may work in most cases, it can also be very dangerous because it relies on the assumption that the template method always calls your operations in that specific order. However, the concrete order in which the primitive operations are called is an implementation detail of the template method and could change every time, which would break your code.
Static Inheritance
The Template-Method relies on static inheritance to provide variations, which has limitations. One drawback is that you cannot easily replace individual steps during runtime, as all steps are bundled within one class.
If you need a more dynamic setup and want to replace parts of the algorithm during runtime, then using the Strategy pattern and injecting every step as strategy into the template method may be the better choice.
Another weakness is that reusing primitive operations between some, but not all, of your concrete classes can be cumbersome. Think again of our coffee shop example: What if two shops share the same payment operation while a third uses a different method? To implement the payment only once and reuse it, you must add an intermediary class that provides the payment method to derived classes. However, doing this could result in a combinatorial explosion of intermediary classes, as you would need one new layer for every new operation you want to share.
For such a scenario, it could be easier to use composition instead and provide each coffee shop with a list of possible payment strategies. The mechanisms of the template-method pattern would be too rigid here.