Template Vs Strategy Pattern
- obanby
- Mar 16, 2021
- 4 min read
Updated: Mar 28, 2021
Intro:
Have you ever written identical code a bazillion times?? Have you ever had to refer to an older module or a different file to reference how you did it before?
No, it doesn't ring a bell? Then get out of here...... 🚶🏻♂️
Well for the rest of us who have implemented quick sort plenty of times while dabbling with different programming languages, we know how that feels.
💡 It doesn't matter if you don't know what a quick sort is; that's irrelevant to learning the concepts here.
Juice:
⚠️ Only focus on the structure, ignore the language details and extra functions.
Let's start with a simple example that looks like this code snippet.
Here is a sample calculator code that I just wrote for this post. It allows the user to enter arbitrary equations, evaluates them, and print the results.
func main() {
bufIn := bufio.NewReader(os.Stdin)
bufOut := bufio.NewWriter(os.Stdout)
scanner := bufio.NewScanner(bufIn)
writer := newWriter(bufOut)
writer("Press Q to exit\\n")
for scanner.Scan() {
input := scanner.Text()
if input == "Q" || input == "q" {
break
}
result := evaluate(input)
writer("Result: " + result + "\\n")
bufIn.Discard(-1)
}
}
> go run .
Press Q to exit
10 + 3
Result: 13.000
12 + 12
Result: 24.000
7 + 9
Result: 16.000
8 + 2
Result: 10.000
9 + 2 + 3
Result: 14.000
1 + 2 + 1
Result: 4.000
This is a very simple code, yet the same pattern is used in so many applications! From calculators, text processing, file parsing, etc...
The pattern is simply:
setup()
loop condition() {
logic()
}
cleanup()
That right there 👆🏼 is literally your template! Hence, the name template pattern.
Template Pattern
"Allow a generic algorithm to manipulate many possible detailed implementations" - Uncle bob
"Generic algorithm" refers to the piece of code you want to template. In our example, it's the loop with, condition, logic and cleanup.
💡 Notice the singularity here! A single algorithm can manipulate multiple implementations.
Shhhhh SHOW ME THE CODE!
public abstract class App {
private boolean isDone = false;
protected void setDone() {
isDone = true;
}
protected boolean isDone() {
return isDone;
}
protected abstract void setup();
protected abstract void logic();
protected abstract void cleanup();
public void run() {
setup();
while (!isDone())
logic();
cleanup();
}
}
public class Calculator extends App {
// declare your buffers
public static void main(String[] args) {
// running the run function from App (template) class
(new Calculator()).run();
}
protected void init() {
// assign your buffers
}
protected void logic() {
// You implement the logic here
// if done computing
// setDone()
}
protected void cleanup() {
// Here you close the streams
// and run clean up code you like
}
}
Why show you pseudocode??
When learning patterns, it is important to spot them in any language.
Golang doesn't support function override. Therefore, I can't demonstrate it in Go!
Most interviews will be conducted via whiteboard where you will need to use pseudocode, so master it now!
Strategy Pattern
So what is Strategy Pattern?
"Strategy pattern allows many details implementation to be manipulated by many different generic algorithms" - Uncle Bob
Strategy pattern allows you to reuse your code through the dependency inversion principle. Meaning, your code complies with an interface. Then, another class implements the algorithm utilizing that interface too.
type App interface {
setup()
condition() bool
logic()
cleanup()
}
type AppRunner struct {
App
}
func (a *AppRunner) run() {
a.setup()
for a.condition() {
a.logic()
}
a.cleanup()
}
Now all you need is for your code to implement the App Interface, and you are ready to execute it with any other algorithm structure you wish.
type Calculator struct{}
func (c *Calculator) setup() {
bufIn = bufio.NewReader(os.Stdin)
scanner = bufio.NewScanner(bufIn)
writer = newWriter(bufio.NewWriter(os.Stdout))
writer("Press Q to exit\\n")
writer("Enter values\\n")
}
func (c *Calculator) condition() bool {
return scanner.Scan()
}
func (c *Calculator) logic() {
input := scanner.Text()
if input == "Q" || input == "q" {
os.Exit(0)
}
result := evaluate(input)
writer("Result: " + result + "\\n")
}
func (c *Calculator) cleanup() {
writer("Thank you for using the most simple calc ever\\nGood bye\\n")
}
Your main function looks so elegant now!
func main() {
calc := AppRunner{new(Calculator)}
calc.run()
}
Say you want to run the clean up multiple times like this:
cleanup()
setup()
for condition() {
logic()
}
cleanup()
Then all you need to do is this:
type CleanUpRunner struct {
App
}
func (a *CleanUpRunner) run() {
a.cleanup()
a.setup()
for a.condition() {
a.logic()
}
a.cleanup()
}
func main() {
calc := CleanUpRunner{new(Calculator)}
calc.run()
}
This is particularly powerful when you want to have a pluggable system. For example, using different sort algorithms or multiple caching systems for various cases!
💡 Notice the plural “many detailed implementations" are manipulated by “many different algorithms.” This will allow decoupling; you can use the same detailed implementation and integrate it with a different setup.
Footnotes:
If you are still confused after reading this blog, that's okay! You will definitely understand by reading “Uncle Bob” so make sure you read "Agile Software Development, Principles, Patterns, and Practices" to learn more.
Please email, tweet, or dm me if anything requires improvement or if you would like to chat!
lastly thank you for reading through this blog post! You are rare and I wish you joy :cheers: 🍻
Comments