Software Design Pattern By Example: Template Method
Explain Template Method Design Pattern in an easy-to-understand example
Table of Contents:
· The Problem: Safe Save
· Analyse & Prepare
· Sub-optimal Design
∘ Strategy pattern not optimal here
· Template Method Pattern
- Python Version
Example Problem: Safe Save
An online editor has a ‘Save Project’ button, which will save all open files. The rules for our safe save are:
The syntax must be valid for certain plain-text files (e.g. XML, Ruby scripts)
Try to reformat (pretty-print) the document, but only if the syntax is valid and feasible
Note: the actual code logic of validation/reformatting is not necessary, just print a line that represents the process.
Here is a sample output of ‘Save Project’ (files: foo.xml
, wise.pdf
, bar.rb
):
[XmlFile] Validate
[XmlFile] Reformatting ...
{foo.xml} Saving ...
[PdfFile] Skip validate
{wise.pdf} not saved ...
[RubyFile] Validate
[RubyFile] Reformatting ...
{bar.rb} Saving ...
So how can we design the safe ‘Save Project’ button in an object-oriented language?
Analyse & Prepare
A list of project files
We need a data structure to store a list of project files. A good candidate isstd:list
from the C++ Standard Template Library (STL).
std::list<std::string> products;
products.push_back("foo");
products.push_back("bar");
products.insert(0, "hello world");
// products => ["hello world", "foo", "bar"]
Traverse a list
The below is a C+11 (and later) way of traversing a list.
for (auto prod : products)
std::cout << prod << "\n"
Two save methods
Onesave()
method does the actual file saving, the othersafe_save()
validates the file content (and apply reformatting if it is okay) then callsave()
. The content of two other methodsvalidate()
andreformat()
are just a single line of printing text.
Sub-optimal Design
Let’s look at a typical design from programmers new to Object Oriented Programming.
In main.cpp
,
std::list<ProjectFile*> project_files;
ProjectFile* xml_file = new ProjectFile("foo.xml")
ProjectFile* pdf_file = new ProjectFile("wise.pdf")
ProjectFile* ruby_file = new ProjectFile("bar.rb")
project_files.push_back(xml_file);
project_files.push_back(pdf_file);
project_files.push_back(ruby_file);
for (auto pf : project_files)
pf->save_save();
In the ProjectFile
class,
void ProjectFile::safe_save() {
if (validate()) {
reformat();
save();
} else {
std::count << "{" << file_path << "} not saved...\n";
}
}
void ProjectFile::save() {
std::cout << "{" << file_path << "} Saving...\n";
}
bool ProjectFile::validate() {
if (file_path.find(".xml") != std::string::npos) {
std::cout << "[XmlFile] Validate\n";
} else if (file_path.find(".rb) != std::string::npos) {
std::cout << "[RubyFile] Validate\n";
} else if (file_path.find(".pdf") != std::string::npos) {
return false;
}
return true;
}
void ProjectFile::reformat() {
if (file_path.find(".xml) != std::string::npos) {
std::cout << "[XmlFile] Reformatting ...\n";
} else if (file_path.find(".rb") != std::string::npos) {
std::cout << "[RubyFile] Reformatting ... \n";
}
}
It is very easy for novice programmers to write code like the above. The design has the following shortcomings.
Too many
if
statements, which is a sign for ‘code horror’Not easy to add support for a new file type
Different validation/reformatting code (for different file types) are all in the same file
Strategy pattern not optimal here
Some might use the Strategy pattern to approach this by subclassing project files.
std::list<ProjectFile*> project_files;
ProjectFile* xml_file = new XmlFile("foo.xml");
ProjectFile* pdf_file = new PdfFile("wise.pdf);
ProjectFile* ruby_file = new RubyFile("bar.rb);
project_files.push_back(xml_file);
project_files.push_back(pdf_file);
project_files.push_back(ruby_file);
for (auto pf : project_files)
pf->save_save();
This is a good direction. However, if you make one virtual safe_save
method in ProjectFile and add implementations in multiple child classes like the below:
class ProjectFile {
public:
virtual void safe_save() = 0;
}
class RubyFile : ProjectFile {
void safe_save() override;
void validate();
void reformat();
};
class XmlFile : ProjectFile {
void safe_save() override;
void validate();
void reformat();
};
In RubyFile
and XmlFile
, we will see duplications in safe_save()
:
void RubyFile::safe_save() {
if (validate()) {
reformat();
save();
} else {
std::cout << "{" << file_path << "} not saved ...\n";
}
}
// ...
void XmlFile::safe_save() {
if (validate()) {
reformat();
save();
} else {
std::cout << "{" << file_path << "} not saved ...\n";
}
}
The solution is near: using the Template Method pattern.
Template Method Pattern
The Template Method is a Gang of Four (GoF) design pattern.
In ProjectFile.cpp
, the methods safe_save()
and save()
should be implemented (rather than a virtual method), because the logic applies to all project files. The code for these two methods is the same as the first version. Instead, we make validate()
and reformat()
methods virtual. This makes sense, as the code logic of validating and reformatting is specific to file types.
class ProjectFile {public:
void safe_save();protected:
virtual bool validate() = 0;
virtual void reformat() = 0;
};
The implementations are in a sub-class, e.g. RubyFile
.
bool RubyFile::validate() {
std::cout << "[RubyFile] Validate\n";
return true;
}
void RubyFile::reformat() {
std::cout << "[RubyFile] Reformatting ...\n";
}
and PdfFile
, which does not support validation.
bool PdfFile::validate() {
std::cout << "[PdfFile] Skip validate\n";
return false;
}
The ProjectFile
‘s safe_save
method in the parent class is also of note:
void ProjectFile::safe_save() {
if (validate()) {
reformat();
save();
} else {
std::cout << "{" << file_path << "} not saved ...\n";
}
}
This method calls three other methods:
validate()
— a virtual method, the implementation provided in child classesreformat()
— a virtual method, the implementation provided in child classessave()
— an instance method that applies to all project files (including child classes)
The safe_save()
method defines the logic or a template of how this operation shall do at the overall level. While some of the specific implementations are delegated to its child classes. That’s why it is called the Template Method pattern.
Python Version
This article’s code was written in C++.
I also have a Python version (link here and below) of the same problem for those more familiar.
class ProjectFile:
safe_save = 0
def __init__(self, file_name):
self.file_name = file_name
def save(self):
print("{", self.file_name, "} Saving ...")
def safe_save(self):
if (self.validate()):
self.reformat()
self.save()
else:
print("{", self.file_name, "} Not saved")
class RubyFile(ProjectFile):
def validate(self):
print("[RubyFile] Validate")
return True
def reformat(self):
print("[RubyFile] Reformatting")
class XmlFile(ProjectFile):
def validate(self):
print("[XmlFile] Validate")
return True
def reformat(self):
print("[XmlFile] Reformatting")
class PdfFile(ProjectFile):
def validate(self):
print("[PdfFile] Skip Validate")
return False
def reformat(self):
print("[PdfFile] Reformatting")
ruby_file = RubyFile("bar.rb")
xml_file = XmlFile("foo.xml")
pdf_file = PdfFile("wise.pdf")
xml_file.safe_save()
pdf_file.safe_save()
ruby_file.safe_save()
Related reading