Template Method Pattern — A Quick Guide
Design Patterns: Template Method Pattern
--
“Template Method Pattern defines the skeleton of an algorithm in a method, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.”
Example:
Let’s say you have an application that helps you deploy your applications to Staging or Production Environments, like this:
public class StagingDeploy {
public void deploy() {
runUnitTests();
pushSemanticVersioningTag();
pushTaggedImage();
saveImageToRegistryAsBackup();
} void runUnitTests() {
// implementation for unit tests
}
void pushSemanticVersioningTag() {
// implementation for semantic versioning
}
void pushTaggedImageToStaging() {
// implementation for pushing tagged image to staging
}
void saveImageToRegistryAsBackup() {
// implementation for saving image as backup
}
}
public class ProductionDeploy {
public void deploy() {
runSmokeTests();
pushReleaseTag();
pushTaggedImage();
saveImageToRegistryAsBackup();
} void runSmokeTests() {
// implementation for smoke tests
}
void pushReleaseTag() {
// implementation for release versioning
} void pushTaggedImageToProduction() {
// implementation for pushing tagged image to production
} void saveImageToRegistryAsBackup() {
// implementation for saving image as backup
}
}
There are clear similarities between Staging and Production deploys, so you can create a template for both of them to use.
Implementation
public abstract class Deploy {
public void final deploy() {
runTests();
pushTag();
pushTaggedImage();
saveImage();
} abstract void runTests();
abstract void pushTag();
abstract void pushTaggedImage(); abstract void saveImage();
}
and now you can extend it like this:
public class StagingDeploy extends Deploy {
@Override
void runTests() {
// implementation for unit tests
}
@Override
void pushTag() {
// implementation for semantic versioning
}
@Override
void pushTaggedImage() {
// implementation for pushing tagged image to staging
}
@Override
void saveImage() {
// implementation for saving image as backup
}
}public class ProductionDeploy extends Deploy {
@Override
void runTests() {
// implementation for smoke tests
}
@Override
void pushTag() {
// implementation for release versioning
}
@Override
void pushTaggedImage() {
// implementation for pushing tagged image to production
}
@Override
void saveImage() {
// implementation for saving image as backup
}
}
And the client code would be like:
StagingDeploy stagingDeploy = new StagingDeploy();
stagingDeploy.deploy();
ProductionDeploy productionDeploy = new ProductionDeploy();
productionDeploy.deploy();
For this case, you may have realized saveImage() method is the same for both classes. So you can change the code like this:
public abstract class Deploy {
public void final deploy() {
runTests();
pushTag();
pushTaggedImage();
saveImage();
} abstract void runTests();
abstract void pushTag();
abstract void pushTaggedImage(); void saveImage() {
// implementation for saving image as backup
}}
public class StagingDeploy extends Deploy {
@Override
void runTests() {
// implementation for unit tests
}
@Override
void pushTag() {
// implementation for semantic versioning
}
@Override
void pushTaggedImage() {
// implementation for pushing tagged image to staging
}
}public class ProductionDeploy extends Deploy {
@Override
void runTests() {
// implementation for smoke tests
}
@Override
void pushTag() {
// implementation for release versioning
}
@Override
void pushTaggedImage() {
// implementation for pushing tagged image to production
}
}
Hooks
There is also another concept called Hooks.
Hooks are methods in abstract classes that can be overriden, but not have to.
Suppose you want to add “performance tests” step to our template but you don’t want it to run in Staging and Production environments.
Let’s say you now have a new environment that crucial apps work and you want to make sure the applications deployed here runs faster than 2ms.
public abstract class Deploy {
public void final deploy() {
runPerformanceTests();
runTests();
pushTag();
pushTaggedImage();
saveImage();
} void runPerformanceTests() {
// empty
} abstract void runTests();
abstract void pushTag();
abstract void pushTaggedImage(); void saveImage() {
// implementation for saving image as backup
}
}public class StagingDeploy extends Deploy {
@Override
void runTests() {
// implementation for unit tests
}
@Override
void pushTag() {
// implementation for semantic versioning
}
@Override
void pushTaggedImage() {
// implementation for pushing tagged image to staging
}
}public class ProductionDeploy extends Deploy {
@Override
void runTests() {
// implementation for smoke tests
}
@Override
void pushTag() {
// implementation for release versioning
}
@Override
void pushTaggedImage() {
// implementation for pushing tagged image to production
}
}public class NewProductionDeploy extends Deploy {
@Override
void runPerformanceTests() {
// implementation for performance tests
} @Override
void runTests() {
// implementation for both unit & smoke tests
}
@Override
void pushTag() {
// implementation for release versioning
}
@Override
void pushTaggedImage() {
// implementation for pushing tagged image to new production
}
}
Hooks: You CAN override the method to add behavior, but you don’t have to.
Abstract Methods: You MUST override the method.
Things to Consider
While Template Method Pattern is great and widely being used (sometimes without even realizing), we should think twice before using it:
1. Always favor composition over inheritance.
Before using Template Method Pattern, check if Strategy Pattern could be a better fit. Template Method Pattern works at the class level, so it’s a bit more static. Strategy Pattern works on the object level, letting you switch behaviors at runtime.
2. Be aware that you may violate the Liskov Substitution Principle.
LSP basically says that objects of a superclass shall be replaceable with objects of its subclasses without breaking the application. While it sounds easy, as the application gets larger in the future, we may unknowingly modify the template to not be suitable for all subclasses.
3. Be aware that you may violate the Open-Closed Principle.
OCD basically says that classes should be open for extension, but closed for modification. As classes get larger, maintaining the template for each class becomes a problem and instead of classes adapting themselves to the template, things turn around and template starts to adapt itself to classes.
Thank you for reading and being a part of my journey!
Reference(s)
- Head First Design Patterns. by Eric Freeman, Elisabeth Robson, Bert Bates, Kathy Sierra. Released October 2004. Publisher(s): O’Reilly Media, Inc.
- Dive into Design Patterns. by Alexander Shvets, Released 2019.