Design Patterns in Go: Template Method
Greetings, most excellent fellow developer!
I did an intro look at the Go object-oriented system and how it’s different from the traditional inheritance/polymorphism model in the second life of OOP post.
From time to time when I’m learning a new language and want to test its boundaries I like implementing design patterns.
I treat them as the “leetcode” of program design i.e. small problems that have a short well-defined solution, but instead of trying to get the best time or memory complexity score, you focus on future-proofing you code.
Preamble
Engineers are really good at generalising.
That’s what I’ve learnt from doing tech talks and learning from other people experiences.
This means however, that we often struggle with going the other way and finding an application for a clever idea.
Think of the time when you had math lessons and learnt a new theorem.
Did you think, - “Yes, that’s what I need to make them backend services work”?
I bet you didn’t.
So, I’ll do what’s natural and start with a concrete problem and then iterate and generalise the solution.
Day 1: CSV Encoder
Imagine a piece of code that was written once to solve a very specific problem, never updated, and now you own it and have to maintain and evolve.
I present you the DataTable.
It’s pretty simple and only does two things:
- Stores a 2d array of a given type representing a table.
- Knows how to print it out in a CSV format.
package main
import "fmt"
type DataTable[T any] struct {
// use generics here so that it's agnostic to type
data [][]T
}
// constructor function
func NewTable[T any](data [][]T) *DataTable[T] {
return &DataTable[T] {
data: data,
}
}
// ------------ output in CSV format ------------
func (t *DataTable[T]) Print() {
for _, row := range t.data { // row by row
for idx, col := range row { // every column
fmt.Print(col) // print value
if idx < len(row) - 1 {
fmt.Print(",")
}
}
fmt.Println()
}
}
// ------------ -------------------- ------------
func main() {
table := NewTable([][]string{
{"A", "B", "C"},
{"D", "E", "F"},
})
table.Print()
}
You can run this in Go Playground and see the formatted output:
A,B,C
D,E,F
You get to know the code and you finish Day 1 with a thought of work well done 😉
Day 2: CSV to HTML
It’s a nice morning at DataTable Ltd and you’re half way through your coconut cappuccino ☕️
Next minute, your boss says: “Let’s launch DataTable on the Web today! 🚀”
So now this code should be able to output both the CSV and HTML.
In order to get something scrappy fast, I’ll quickly add a few conditions in the output logic:
package main
import "fmt"
import "os"
type DataTable[T any] struct {
data [][]T
}
func NewTable[T any](data [][]T) *DataTable[T] {
return &DataTable[T] {
data: data,
}
}
// ------------ most changes are here ------------
func (t *DataTable[T]) Print(format string) {
if format == "html" {
fmt.Println("<table>")
}
for _, row := range t.data {
if format == "html" {
fmt.Println("<tr>")
}
for idx, col := range row {
if format == "html" {
fmt.Print("<td>", col, "</td>")
} else {
fmt.Print(col)
if idx < len(row) - 1 {
fmt.Print(",")
}
}
}
fmt.Println()
if format == "html" {
fmt.Println("</tr>")
}
}
if format == "html" {
fmt.Println("</table>")
}
}
// ------------ --------------------- ------------
func main() {
table := NewTable([][]string{
{"A", "B", "C"},
{"D", "E", "F"},
})
table.Print(os.Args[1])
}
This gets the job done, but it’s not the prettiest code 😅
Run this code in Go Playground and see the HTML output:
<table>
<tr>
<td>A</td><td>B</td><td>C</td>
</tr>
<tr>
<td>D</td><td>E</td><td>F</td>
</tr>
</table>
Imagine also adding 2-3 more different output formats in future:
Day 3: Template Method
Now that the feature is out and we’re head to toe in tech debt, it’s time to refactor everything. 👷
The idea behind the Template pattern is that you have a common algorithm (template) and then customise the details.
Is this case, common logic reflects the data structure: it’s a table, there are rows and columns, so they should be printed out accordingly. And the differences are in how rows and columns are printed i.e. there needs to be an opening and closing tag in case of HTML.
Given that it’s Go, I decided to use interface embedding to emulate classic abstract methods that could be overridden later.
package main
import "fmt"
import "os"
type DataTable[T any] struct {
data [][]T
FormatTemplate
}
// ------- abstract actions in our algorithm -------
type FormatTemplate interface {
Begin()
End()
BeginRow()
EndRow()
BeginValue()
EndValue(last bool)
}
// ------- --------------------------------- -------
func NewTable[T any](data [][]T, format FormatTemplate) *DataTable[T] {
return &DataTable[T] {
data: data,
FormatTemplate: format,
}
}
// ------- common template formatting logic -------
func (t *DataTable[T]) Print() {
t.Begin()
for _, row := range t.data {
t.BeginRow()
for idx, col := range row {
t.BeginValue()
fmt.Print(col)
t.EndValue(idx + 1 == len(row))
}
t.EndRow()
}
t.End()
}
// ------- --------------------------------- -------
// ------- concrete logic specific to CSV -------
type FormatCSV struct {}
func (f FormatCSV) Begin() {}
func (f FormatCSV) End() {}
func (f FormatCSV) BeginRow() {}
func (f FormatCSV) EndRow() {
fmt.Println()
}
func (f FormatCSV) BeginValue() {}
func (f FormatCSV) EndValue(last bool) {
if !last {
fmt.Print(",")
}
}
// ------- --------------------------------- -------
// ------- concrete logic specific to HTML -------
type FormatHTML struct {}
func (f FormatHTML) Begin() {
fmt.Println("<table>")
}
func (f FormatHTML) End() {
fmt.Println("</table>")
}
func (f FormatHTML) BeginRow() {
fmt.Println("<tr>")
}
func (f FormatHTML) EndRow() {
fmt.Println("</tr>")
}
func (f FormatHTML) BeginValue() {
fmt.Print("<td>")
}
func (f FormatHTML) EndValue(last bool) {
fmt.Print("</td>")
if last {
fmt.Println()
}
}
// ------- --------------------------------- -------
func main() {
var format FormatTemplate = FormatCSV{}
if os.Args[1] == "html" {
format = FormatHTML{}
}
table := NewTable([][]string{
{"A", "B", "C"},
{"D", "E", "F"},
}, format)
table.Print()
}
You can see now how we only have a very clean and simple print
method and all the specifics of how to print lines, columns and values are done in concrete format implementations.
Run this code in Go Playground and try adding another formatter!
What we’ve learnt
- You can use Template pattern to simplify conditional logic in your code
- You can write Go implementation that’s similar to the classic OOP despite not having inheritance system
- Implementing design patterns is a fun way to test the boundaries of new tech you’re learing 😇
Thank you for reading 👍
If you like this material, bookmark my blog and visit again :)
You’ve been awesome!
Member discussion