Software Design Patterns by Example: Template Method

What is the Template Method and when should we use it?

Courtney Zhan
4 min readJan 22, 2022
Table of Contents* Example Problem: Safe Save
* Analyse & Prepare
* Sub-optimal Design
- Strategy Pattern is 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:

  1. The syntax must be valid for certain plain-text files (e.g. XML, Ruby scripts)
  2. 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 is std: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
    One save() method does the actual file saving, the other safe_save() validates the file content (and apply reformatting if it is okay) then call save(). The content of two other methods validate() and reformat() are just a single line of printing text.

Sub-optimal Design

Let’s look at a typical design from programmers who are 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 theProjectFile 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 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.

Image Credit: https://reactiveprogramming.io/blog/en/design-patterns/template-method

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. As a matter of fact, the code forthese two methods is exactly the same as the first version. Instead, we make the validate() and reformat() methods virtual. This makes sense, as the code logic of validating and reformatting are 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 classes
  • reformat() — a virtual method, the implementation provided in child classes
  • save() — 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 with it.

--

--

No responses yet