Table of contents
The Template Method Pattern is a behavioral design pattern that defines the skeleton of an algorithm in a superclass but lets subclasses override specific steps of the algorithm without changing its structure. In other words, the Template Method Pattern provides a template for performing an operation but allows the subclasses to modify certain steps of the operation without changing its overall structure.
The Template Method Pattern is a part of the "Gang of Four" design patterns, which were first introduced in their book "Design Patterns: Elements of Reusable Object-Oriented Software". The "Gang of Four" includes Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. These patterns are widely used in software development to solve commonly occurring problems in object-oriented programming.
Example
Let's consider a real-world example of how the Template Method Pattern can be used in Golang programming. Suppose we want to create a program that reads data from different sources and then processes it. The sources can be files, databases, or even remote APIs. We can create an abstract class that defines the template for reading and processing data.
type DataReaderProcessor interface {
ReadData() ([]byte, error)
ProcessData(data []byte) error
GetDataSourceName() string
}
type DataReaderProcessorTemplate struct {
dataSource DataReaderProcessor
}
func (p *DataReaderProcessorTemplate) ReadAndProcessData() error {
data, err := p.dataSource.ReadData()
if err != nil {
return err
}
err = p.dataSource.ProcessData(data)
if err != nil {
return err
}
return nil
}
func (p *DataReaderProcessorTemplate) GetDataSourceName() string {
return p.dataSource.GetDataSourceName()
}
In the above code, we have defined an interface DataReaderProcessor
that specifies the methods required for reading and processing data. We then define a DataReaderProcessorTemplate
struct that implements the DataReaderProcessor
interface. The DataReaderProcessorTemplate
struct provides a template for reading and processing data.
The ReadAndProcessData
method reads data from the data source and processes it by calling the ProcessData
method. The GetDataSourceName
method returns the name of the data source.
Now we can create structs that implement the DataReaderProcessor
interface and implement the ReadData
and ProcessData
methods according to their specific data sources. For example, we can define a FileReaderProcessor
that reads data from a file and a DatabaseReaderProcessor
that reads data from a database.
type FileReaderProcessor struct {
DataReaderProcessorTemplate
fileName string
}
func (p *FileReaderProcessor) ReadData() ([]byte, error) {
data, err := ioutil.ReadFile(p.fileName)
if err != nil {
return nil, err
}
return data, nil
}
func (p *FileReaderProcessor) ProcessData(data []byte) error {
// process data
return nil
}
func (p *FileReaderProcessor) GetDataSourceName() string {
return p.fileName
}
type DatabaseReaderProcessor struct {
DataReaderProcessorTemplate
db *sql.DB
}
func (p *DatabaseReaderProcessor) ReadData() ([]byte, error) {
// read data from database
return nil, nil
}
func (p *DatabaseReaderProcessor) ProcessData(data []byte) error {
// process data
return nil
}
func (p *DatabaseReaderProcessor) GetDataSourceName() string {
return "Database"
}
In the above code, we have defined FileReaderProcessor
and DatabaseReaderProcessor
structs that implement the DataReaderProcessor
interface. These classes implement the ReadData
and ProcessData
methods according to their data sources.
Now we can use these structs to read and process data from different sources. For example, we can create a FileReaderProcessor
object and call its ReadAndProcessData
method to read and process data from a file.
func main() {
processor := &FileReaderProcessor{
DataReaderProcessorTemplate: DataReaderProcessorTemplate{
dataSource: &FileReaderProcessor{
fileName: "/path/to/file",
},
},
}
err := processor.ReadAndProcessData()
if err != nil {
log.Fatalf("Error: %v", err)
}
}
In the above code, we have created a FileReaderProcessor
object and passed it to the DataReaderProcessorTemplate
struct. We then call the ReadAndProcessData
method to read and process data from the file.
Conclusion
The Template Method Pattern provides a way to define the skeleton of an algorithm in a superclass but allows subclasses to modify specific steps of the algorithm without changing its overall structure. This pattern is useful in situations where we want to define a common algorithm but allow subclasses to customize certain steps of the algorithm. In Golang, we can use the Template Method Pattern to create a template for reading and processing data from different sources. The pattern allows us to define a common algorithm for reading and processing data but allows subclasses to customize the specific data source.
References
- “Design Patterns: Elements of Reusable Object-Oriented Software” by Erich Gamma, John Vlissides, Ralph Johnson, and Richard Helm