Strategy Pattern
Part of the Object Oriented Programming section of Coddy's GO journey — lesson 90 of 107.
The Strategy pattern allows you to define a family of algorithms, encapsulate each one, and make them interchangeable at runtime. Unlike Observer where multiple objects react to one event, Strategy lets a single object switch between different behaviors dynamically.
In Go, we implement Strategy by defining an interface for the algorithm and injecting different implementations into a context struct:
type PaymentStrategy interface {
Pay(amount float64) string
}
type CreditCard struct{}
func (c CreditCard) Pay(amount float64) string {
return fmt.Sprintf("Paid %.2f via Credit Card", amount)
}
type PayPal struct{}
func (p PayPal) Pay(amount float64) string {
return fmt.Sprintf("Paid %.2f via PayPal", amount)
}The context struct holds a reference to the strategy interface, allowing the algorithm to be swapped without changing the context's code:
type Checkout struct {
strategy PaymentStrategy
}
func (c *Checkout) SetStrategy(s PaymentStrategy) {
c.strategy = s
}
func (c *Checkout) ProcessPayment(amount float64) string {
return c.strategy.Pay(amount)
}Now you can change the payment method at runtime:
checkout := &Checkout{}
checkout.SetStrategy(CreditCard{})
fmt.Println(checkout.ProcessPayment(100.00)) // Paid 100.00 via Credit Card
checkout.SetStrategy(PayPal{})
fmt.Println(checkout.ProcessPayment(50.00)) // Paid 50.00 via PayPalStrategy is ideal when you have multiple ways to perform an operation and need to select one based on user input, configuration, or runtime conditions. Common use cases include sorting algorithms, compression methods, and validation rules.
Challenge
EasyLet's build a text formatting system using the Strategy pattern! You'll create different formatting strategies that can be swapped at runtime, allowing the same text processor to produce different output styles without changing its core code.
You'll organize your code across three files:
formatter.go: Define your strategy interface and concrete formatting strategies.Create a
TextFormatterinterface with a methodFormat(text string) stringthat transforms text according to the strategy's rules.Implement three formatting strategies:
UppercaseFormatter— converts text to all uppercaseSnakeCaseFormatter— converts spaces to underscores and makes text lowercaseTitleFormatter— capitalizes the first letter of each word (words are separated by spaces)
processor.go: Create your context struct that uses the formatting strategy.Build a
TextProcessorstruct that holds aTextFormatterstrategy. Add these methods:SetFormatter(f TextFormatter)to change the active formatting strategyProcess(text string) stringthat applies the current formatter to the text
Also create a
NewTextProcessor()constructor that returns a processor with no initial formatter set.main.go: Demonstrate switching strategies at runtime.Read a piece of text, then read a count of format operations. For each operation, read the format type (
upper,snake, ortitle), set the appropriate formatter on your processor, process the original text, and print the result.
The following inputs will be provided:
- Line 1: The text to format
- Line 2: Number of format operations
- Following lines: Format type for each operation
For example, given:
hello world today
3
upper
snake
titleYour output should be:
HELLO WORLD TODAY
hello_world_today
Hello World TodayAnd given:
Go Programming Language
2
snake
upperYour output should be:
go_programming_language
GO PROGRAMMING LANGUAGEAnd given:
design patterns are useful
1
titleYour output should be:
Design Patterns Are UsefulNotice how the same TextProcessor produces completely different outputs just by swapping its formatter strategy. The processor doesn't know or care which specific formatter it's using—it simply delegates to whatever strategy is currently set!
Cheat sheet
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime. It allows a single object to switch between different behaviors dynamically.
Define an interface for the algorithm:
type PaymentStrategy interface {
Pay(amount float64) string
}Implement concrete strategies:
type CreditCard struct{}
func (c CreditCard) Pay(amount float64) string {
return fmt.Sprintf("Paid %.2f via Credit Card", amount)
}
type PayPal struct{}
func (p PayPal) Pay(amount float64) string {
return fmt.Sprintf("Paid %.2f via PayPal", amount)
}Create a context struct that holds a reference to the strategy interface:
type Checkout struct {
strategy PaymentStrategy
}
func (c *Checkout) SetStrategy(s PaymentStrategy) {
c.strategy = s
}
func (c *Checkout) ProcessPayment(amount float64) string {
return c.strategy.Pay(amount)
}Change strategies at runtime:
checkout := &Checkout{}
checkout.SetStrategy(CreditCard{})
fmt.Println(checkout.ProcessPayment(100.00)) // Paid 100.00 via Credit Card
checkout.SetStrategy(PayPal{})
fmt.Println(checkout.ProcessPayment(50.00)) // Paid 50.00 via PayPalStrategy is ideal when you have multiple ways to perform an operation and need to select one based on user input, configuration, or runtime conditions.
Try it yourself
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Read the text to format
text, _ := reader.ReadString('\n')
text = strings.TrimSpace(text)
// Read the number of operations
countStr, _ := reader.ReadString('\n')
count, _ := strconv.Atoi(strings.TrimSpace(countStr))
// Create a new text processor
processor := NewTextProcessor()
// Process each format operation
for i := 0; i < count; i++ {
formatType, _ := reader.ReadString('\n')
formatType = strings.TrimSpace(formatType)
// TODO: Based on formatType ("upper", "snake", or "title"),
// set the appropriate formatter on the processor
// and print the processed text
}
}
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 Collection13Design Patterns Part 1
Intro to Design PatternsSingleton PatternFactory PatternAbstract Factory PatternObserver PatternStrategy Pattern2Types & 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 Parser3Pointers & 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