Template Method Pattern
Part of the Object Oriented Programming section of Coddy's GO journey — lesson 94 of 107.
The Template Method pattern defines the skeleton of an algorithm in a base type, letting subtypes override specific steps without changing the overall structure. While Decorator adds behavior by wrapping objects, Template Method controls the algorithm's flow and allows customization of individual steps.
In Go, since there's no inheritance, we implement this pattern using struct embedding combined with interfaces. The "template" struct defines the algorithm structure and calls methods that can be customized:
type DataProcessor interface {
ReadData() string
ProcessData(data string) string
SaveData(result string) string
}
type BaseProcessor struct {
Impl DataProcessor
}
func (b *BaseProcessor) Execute() string {
data := b.Impl.ReadData()
result := b.Impl.ProcessData(data)
return b.Impl.SaveData(result)
}Concrete implementations provide their own versions of each step while the algorithm flow remains unchanged:
type CSVProcessor struct{}
func (c CSVProcessor) ReadData() string { return "csv-data" }
func (c CSVProcessor) ProcessData(d string) string { return "processed-" + d }
func (c CSVProcessor) SaveData(r string) string { return "Saved: " + r }
type JSONProcessor struct{}
func (j JSONProcessor) ReadData() string { return "json-data" }
func (j JSONProcessor) ProcessData(d string) string { return "parsed-" + d }
func (j JSONProcessor) SaveData(r string) string { return "Stored: " + r }The template method Execute orchestrates the steps in a fixed order:
csvProc := &BaseProcessor{Impl: CSVProcessor{}}
fmt.Println(csvProc.Execute()) // Saved: processed-csv-data
jsonProc := &BaseProcessor{Impl: JSONProcessor{}}
fmt.Println(jsonProc.Execute()) // Stored: parsed-json-dataTemplate Method is ideal when you have algorithms that share the same structure but differ in specific steps, such as data import/export pipelines, report generators, or test frameworks.
Challenge
EasyLet's build a report generation system using the Template Method pattern! You'll create a framework where different report types follow the same generation process—gathering data, formatting it, and outputting the result—but each report type customizes these steps differently.
You'll organize your code across three files:
report.go: Define your interface and the base processor that orchestrates the report generation algorithm.Create a
ReportGeneratorinterface with three methods that represent the steps of report generation:GatherData() string— retrieves the raw data for the reportFormatData(data string) string— transforms the data into the report's formatOutputReport(formatted string) string— produces the final output message
Build a
ReportProcessorstruct that holds aReportGeneratorimplementation. Add aGenerate() stringmethod that executes the three steps in order: gather, format, then output—returning the final result.generators.go: Implement concrete report generators that customize each step.Create two report types:
SalesReportwith aRegionfield (string)GatherData()returnssales-data-[region]FormatData(data)returnsSALES REPORT: [data]OutputReport(formatted)returnsPrinted: [formatted]
InventoryReportwith aWarehousefield (string)GatherData()returnsinventory-[warehouse]FormatData(data)returns*** [data] ***OutputReport(formatted)returnsExported: [formatted]
main.go: Demonstrate how the same algorithm structure produces different results based on the implementation.Read the report type (
salesorinventory) and the configuration value (region for sales, warehouse for inventory). Create the appropriate generator, wrap it in aReportProcessor, callGenerate(), and print the result.
The following inputs will be provided:
- Line 1: Report type (
salesorinventory) - Line 2: Configuration value (region name or warehouse name)
For example, given:
sales
NorthYour output should be:
Printed: SALES REPORT: sales-data-NorthAnd given:
inventory
MainHubYour output should be:
Exported: *** inventory-MainHub ***And given:
sales
WestYour output should be:
Printed: SALES REPORT: sales-data-WestNotice how the ReportProcessor always calls the same three steps in the same order, but each report type provides its own implementation of those steps. The algorithm's skeleton stays fixed while the details vary—that's the Template Method pattern in action!
Cheat sheet
The Template Method pattern defines the skeleton of an algorithm in a base type, letting subtypes override specific steps without changing the overall structure.
In Go, implement this pattern using struct embedding combined with interfaces. The "template" struct defines the algorithm structure and calls methods that can be customized:
type DataProcessor interface {
ReadData() string
ProcessData(data string) string
SaveData(result string) string
}
type BaseProcessor struct {
Impl DataProcessor
}
func (b *BaseProcessor) Execute() string {
data := b.Impl.ReadData()
result := b.Impl.ProcessData(data)
return b.Impl.SaveData(result)
}Concrete implementations provide their own versions of each step while the algorithm flow remains unchanged:
type CSVProcessor struct{}
func (c CSVProcessor) ReadData() string { return "csv-data" }
func (c CSVProcessor) ProcessData(d string) string { return "processed-" + d }
func (c CSVProcessor) SaveData(r string) string { return "Saved: " + r }
type JSONProcessor struct{}
func (j JSONProcessor) ReadData() string { return "json-data" }
func (j JSONProcessor) ProcessData(d string) string { return "parsed-" + d }
func (j JSONProcessor) SaveData(r string) string { return "Stored: " + r }The template method orchestrates the steps in a fixed order:
csvProc := &BaseProcessor{Impl: CSVProcessor{}}
fmt.Println(csvProc.Execute()) // Saved: processed-csv-data
jsonProc := &BaseProcessor{Impl: JSONProcessor{}}
fmt.Println(jsonProc.Execute()) // Stored: parsed-json-dataTemplate Method is ideal when algorithms share the same structure but differ in specific steps, such as data import/export pipelines, report generators, or test frameworks.
Try it yourself
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Read report type
scanner.Scan()
reportType := scanner.Text()
// Read configuration value (region or warehouse)
scanner.Scan()
configValue := scanner.Text()
// TODO: Create the appropriate generator based on reportType
// - If reportType is "sales", create a SalesReport with Region set to configValue
// - If reportType is "inventory", create an InventoryReport with Warehouse set to configValue
// TODO: Create a ReportProcessor with the generator
// TODO: Call Generate() and print the result
_ = reportType
_ = configValue
fmt.Println("TODO: Generate and print the report")
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of Go OOP
External FilesGo Workspace & ModulesPackages & ImportsExported vs Unexported NamesIntroduction to OOP in GoStructs as ClassesDefining Methods on StructsPointer vs Value ReceiversStruct InitializationConstructor FunctionsRecap - Simple Calculator4Interfaces
Introduction to InterfacesImplicit ImplementationInterface as ContractEmpty Interface (any)Type AssertionType SwitchInterface CompositionStringer & Error InterfacesRecap - Shape Calculator7Encapsulation
Exported vs Unexported FieldsPackage-Level EncapsulationGetter & Setter MethodsInformation Hiding in GoRecap - Student Records10Generics (Go 1.18+)
Introduction to GenericsType ParametersType ConstraintsGeneric StructsGeneric Methods WorkaroundRecap - Generic Collection2Types & Structs Deep Dive
Basic & Composite TypesCustom Type DefinitionsStruct TagsAnonymous StructsNested StructsZero Values & DefaultsRecap - Contact Book5Composition Over Inheritance
Why Go Has No InheritanceStruct Embedding BasicsMethod PromotionEmbedding Multiple StructsEmbedding vs AggregationShadowing Embedded MethodsRecap - Employee Hierarchy8Error Handling & OOP
The error InterfaceCustom Error TypesError Wrapping (fmt.Errorf)Sentinel Errorserrors.Is() and errors.As()Panic, Defer, and RecoverRecap - File Parser11Standard Library & OOP
io.Reader & io.Writersort.Interfacefmt.Stringer Interfaceencoding/json with Structshttp.Handler InterfaceRecap - REST API Models14Design Patterns Part 2
Command PatternAdapter PatternDecorator PatternTemplate Method PatternState PatternComposite PatternMiddleware as Decorator3Pointers & Memory
Pointer Basics in GoPointers to StructsPass by Value vs ReferenceThe new() FunctionGarbage Collection in GoRecap - Linked List Builder6Polymorphism in Go
Polymorphism via InterfacesDuck Typing in GoInterface Satisfaction RulesPolymorphic CollectionsDependency InjectionRecap - Payment Processor9Concurrency & OOP
Goroutines BasicsChannels & CommunicationBuffered vs Unbuffered ChanSelect Statementsync.Mutex & sync.RWMutexsync.WaitGroupThread-Safe Struct DesignRecap - Worker Pool