What is Strategy Pattern?
The Strategy pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets you select algorithms at runtime without altering the code that uses them. Think of it like choosing different routes to reach the same destination - each route (strategy) gets you there, but some might be faster, cheaper, or more scenic.
I’ll show you how this pattern can make your Go code more flexible and maintainable through practical examples.
Let’s start with a scenario: Data Compression
Imagine you’re building a file storage service that needs to compress files before storing them. Different file types work better with different compression algorithms: text files with gzip, images with specialized algorithms, and archives with high-compression methods.
Without Strategy Pattern
Here’s how you might handle this without the Strategy pattern:
type FileCompressor struct{}
func (f *FileCompressor) CompressFile(data []byte, fileType string) []byte {
switch fileType {
case "text":
// Gzip compression logic
return f.gzipCompress(data)
case "image":
// Image-specific compression
return f.imageCompress(data)
case "archive":
// High compression for archives
return f.highCompress(data)
default:
return data // No compression
}
}
func (f *FileCompressor) gzipCompress(data []byte) []byte {
// Gzip implementation
return data // Simplified
}
func (f *FileCompressor) imageCompress(data []byte) []byte {
// Image compression implementation
return data // Simplified
}
func (f *FileCompressor) highCompress(data []byte) []byte {
// High compression implementation
return data // Simplified
}
This approach has problems: it violates the Open/Closed Principle, becomes hard to test individual algorithms, and grows complex as you add more compression types.
With Strategy Pattern
Let’s refactor using the Strategy pattern:
package main
import (
"bytes"
"compress/gzip"
"fmt"
"io"
)
// Strategy interface
type CompressionStrategy interface {
Compress(data []byte) ([]byte, error)
Decompress(data []byte) ([]byte, error)
GetName() string
}
// Context that uses strategies
type FileCompressor struct {
strategy CompressionStrategy
}
func NewFileCompressor(strategy CompressionStrategy) *FileCompressor {
return &FileCompressor{strategy: strategy}
}
func (f *FileCompressor) SetStrategy(strategy CompressionStrategy) {
f.strategy = strategy
}
func (f *FileCompressor) CompressData(data []byte) ([]byte, error) {
if f.strategy == nil {
return data, nil // No compression
}
return f.strategy.Compress(data)
}
func (f *FileCompressor) DecompressData(data []byte) ([]byte, error) {
if f.strategy == nil {
return data, nil
}
return f.strategy.Decompress(data)
}
// Concrete Strategies
type GzipStrategy struct{}
func (g *GzipStrategy) Compress(data []byte) ([]byte, error) {
var buf bytes.Buffer
writer := gzip.NewWriter(&buf)
_, err := writer.Write(data)
if err != nil {
return nil, err
}
err = writer.Close()
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (g *GzipStrategy) Decompress(data []byte) ([]byte, error) {
reader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, err
}
defer reader.Close()
return io.ReadAll(reader)
}
func (g *GzipStrategy) GetName() string {
return "gzip"
}
type NoCompressionStrategy struct{}
func (n *NoCompressionStrategy) Compress(data []byte) ([]byte, error) {
return data, nil
}
func (n *NoCompressionStrategy) Decompress(data []byte) ([]byte, error) {
return data, nil
}
func (n *NoCompressionStrategy) GetName() string {
return "none"
}
type MockHighCompressionStrategy struct{}
func (m *MockHighCompressionStrategy) Compress(data []byte) ([]byte, error) {
// Simulate high compression by reducing size (mock implementation)
if len(data) > 10 {
return data[:len(data)/2], nil
}
return data, nil
}
func (m *MockHighCompressionStrategy) Decompress(data []byte) ([]byte, error) {
// Mock decompression - just duplicate the data
result := make([]byte, len(data)*2)
copy(result, data)
copy(result[len(data):], data)
return result, nil
}
func (m *MockHighCompressionStrategy) GetName() string {
return "high-compression"
}
func main() {
data := []byte("This is a sample text that we want to compress using different strategies.")
// Create compressor with gzip strategy
compressor := NewFileCompressor(&GzipStrategy{})
compressed, err := compressor.CompressData(data)
if err != nil {
fmt.Printf("Compression error: %v\n", err)
return
}
fmt.Printf("Original size: %d bytes\n", len(data))
fmt.Printf("Compressed size with %s: %d bytes\n",
compressor.strategy.GetName(), len(compressed))
// Switch to no compression strategy
compressor.SetStrategy(&NoCompressionStrategy{})
compressed2, _ := compressor.CompressData(data)
fmt.Printf("Compressed size with %s: %d bytes\n",
compressor.strategy.GetName(), len(compressed2))
// Switch to high compression strategy
compressor.SetStrategy(&MockHighCompressionStrategy{})
compressed3, _ := compressor.CompressData(data)
fmt.Printf("Compressed size with %s: %d bytes\n",
compressor.strategy.GetName(), len(compressed3))
}
Advanced Strategy with Factory
Let’s create a more sophisticated version with a strategy factory:
package main
import (
"fmt"
"strings"
)
// Enhanced strategy interface
type ProcessingStrategy interface {
Process(data string) string
GetDescription() string
GetCost() float64 // Processing cost
}
// Strategy factory
type StrategyFactory struct {
strategies map[string]func() ProcessingStrategy
}
func NewStrategyFactory() *StrategyFactory {
factory := &StrategyFactory{
strategies: make(map[string]func() ProcessingStrategy),
}
// Register default strategies
factory.Register("uppercase", func() ProcessingStrategy { return &UppercaseStrategy{} })
factory.Register("lowercase", func() ProcessingStrategy { return &LowercaseStrategy{} })
factory.Register("reverse", func() ProcessingStrategy { return &ReverseStrategy{} })
factory.Register("encrypt", func() ProcessingStrategy { return &SimpleEncryptStrategy{} })
return factory
}
func (f *StrategyFactory) Register(name string, creator func() ProcessingStrategy) {
f.strategies[name] = creator
}
func (f *StrategyFactory) Create(name string) ProcessingStrategy {
if creator, exists := f.strategies[name]; exists {
return creator()
}
return nil
}
func (f *StrategyFactory) ListAvailable() []string {
var names []string
for name := range f.strategies {
names = append(names, name)
}
return names
}
// Context with enhanced features
type TextProcessor struct {
strategy ProcessingStrategy
history []string
}
func NewTextProcessor() *TextProcessor {
return &TextProcessor{
history: make([]string, 0),
}
}
func (t *TextProcessor) SetStrategy(strategy ProcessingStrategy) {
t.strategy = strategy
}
func (t *TextProcessor) Process(data string) string {
if t.strategy == nil {
return data
}
result := t.strategy.Process(data)
t.history = append(t.history, fmt.Sprintf("Used %s: %s -> %s",
t.strategy.GetDescription(), data, result))
return result
}
func (t *TextProcessor) GetHistory() []string {
return t.history
}
func (t *TextProcessor) GetProcessingCost() float64 {
if t.strategy == nil {
return 0
}
return t.strategy.GetCost()
}
// Concrete Strategies
type UppercaseStrategy struct{}
func (u *UppercaseStrategy) Process(data string) string {
return strings.ToUpper(data)
}
func (u *UppercaseStrategy) GetDescription() string {
return "Uppercase Converter"
}
func (u *UppercaseStrategy) GetCost() float64 {
return 0.01 // Very cheap operation
}
type LowercaseStrategy struct{}
func (l *LowercaseStrategy) Process(data string) string {
return strings.ToLower(data)
}
func (l *LowercaseStrategy) GetDescription() string {
return "Lowercase Converter"
}
func (l *LowercaseStrategy) GetCost() float64 {
return 0.01
}
type ReverseStrategy struct{}
func (r *ReverseStrategy) Process(data string) string {
runes := []rune(data)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
func (r *ReverseStrategy) GetDescription() string {
return "Text Reverser"
}
func (r *ReverseStrategy) GetCost() float64 {
return 0.02 // Slightly more expensive
}
type SimpleEncryptStrategy struct{}
func (s *SimpleEncryptStrategy) Process(data string) string {
// Simple Caesar cipher with shift of 3
result := make([]rune, len(data))
for i, char := range data {
if char >= 'a' && char <= 'z' {
result[i] = 'a' + (char-'a'+3)%26
} else if char >= 'A' && char <= 'Z' {
result[i] = 'A' + (char-'A'+3)%26
} else {
result[i] = char
}
}
return string(result)
}
func (s *SimpleEncryptStrategy) GetDescription() string {
return "Simple Encryption"
}
func (s *SimpleEncryptStrategy) GetCost() float64 {
return 0.10 // More expensive operation
}
// Strategy selector based on criteria
func SelectOptimalStrategy(factory *StrategyFactory, data string, maxCost float64) ProcessingStrategy {
availableStrategies := factory.ListAvailable()
for _, name := range availableStrategies {
strategy := factory.Create(name)
if strategy.GetCost() <= maxCost {
return strategy
}
}
return nil // No suitable strategy found
}
func main() {
factory := NewStrategyFactory()
processor := NewTextProcessor()
data := "Hello, World!"
// Process with different strategies
strategies := []string{"uppercase", "lowercase", "reverse", "encrypt"}
for _, strategyName := range strategies {
strategy := factory.Create(strategyName)
if strategy != nil {
processor.SetStrategy(strategy)
result := processor.Process(data)
fmt.Printf("Strategy: %s\n", strategy.GetDescription())
fmt.Printf("Input: %s\n", data)
fmt.Printf("Output: %s\n", result)
fmt.Printf("Cost: $%.3f\n", strategy.GetCost())
fmt.Println("---")
}
}
// Select optimal strategy based on cost
fmt.Println("Selecting optimal strategy with max cost $0.05:")
optimalStrategy := SelectOptimalStrategy(factory, data, 0.05)
if optimalStrategy != nil {
processor.SetStrategy(optimalStrategy)
result := processor.Process(data)
fmt.Printf("Selected: %s -> %s\n", optimalStrategy.GetDescription(), result)
}
// Show processing history
fmt.Println("\nProcessing History:")
for i, entry := range processor.GetHistory() {
fmt.Printf("%d. %s\n", i+1, entry)
}
}
Functional Strategy Pattern (Go Idiomatic)
Here’s a more Go-idiomatic approach using functions as strategies:
package main
import (
"fmt"
"sort"
"strings"
)
// Strategy as function type
type SortStrategy func([]string) []string
// Context using functional strategies
type StringSorter struct {
strategy SortStrategy
}
func NewStringSorter(strategy SortStrategy) *StringSorter {
return &StringSorter{strategy: strategy}
}
func (s *StringSorter) SetStrategy(strategy SortStrategy) {
s.strategy = strategy
}
func (s *StringSorter) Sort(data []string) []string {
if s.strategy == nil {
return data
}
// Make a copy to avoid modifying original
result := make([]string, len(data))
copy(result, data)
return s.strategy(result)
}
// Strategy functions
func AlphabeticalSort(data []string) []string {
sort.Strings(data)
return data
}
func LengthSort(data []string) []string {
sort.Slice(data, func(i, j int) bool {
return len(data[i]) < len(data[j])
})
return data
}
func ReverseAlphabeticalSort(data []string) []string {
sort.Sort(sort.Reverse(sort.StringSlice(data)))
return data
}
func CaseInsensitiveSort(data []string) []string {
sort.Slice(data, func(i, j int) bool {
return strings.ToLower(data[i]) < strings.ToLower(data[j])
})
return data
}
// Strategy factory using map
var SortStrategies = map[string]SortStrategy{
"alphabetical": AlphabeticalSort,
"length": LengthSort,
"reverse_alphabetical": ReverseAlphabeticalSort,
"case_insensitive": CaseInsensitiveSort,
}
func main() {
data := []string{"banana", "Apple", "cherry", "date", "elderberry", "fig"}
fmt.Printf("Original data: %v\n\n", data)
// Test different sorting strategies
for name, strategy := range SortStrategies {
sorter := NewStringSorter(strategy)
result := sorter.Sort(data)
fmt.Printf("%s sort: %v\n", name, result)
}
// Dynamic strategy selection
fmt.Println("\nDynamic strategy selection:")
sorter := NewStringSorter(nil)
// Choose strategy based on data characteristics
if len(data) > 5 {
sorter.SetStrategy(SortStrategies["length"])
fmt.Println("Large dataset - using length sort")
} else {
sorter.SetStrategy(SortStrategies["alphabetical"])
fmt.Println("Small dataset - using alphabetical sort")
}
result := sorter.Sort(data)
fmt.Printf("Result: %v\n", result)
}
Real-world Use Cases
I frequently use the Strategy pattern in these scenarios:
- Payment Processing: Different payment gateways (Stripe, PayPal, Square)
- Data Validation: Different validation rules based on user types
- Caching Strategies: Memory, Redis, or database caching
- Authentication: OAuth, JWT, API keys
- File Processing: Different parsers for CSV, JSON, XML
- Pricing Algorithms: Different pricing strategies for different customer segments
Benefits of Strategy Pattern
- Runtime Algorithm Selection: Choose algorithms dynamically
- Open/Closed Principle: Easy to add new strategies without changing existing code
- Testability: Each strategy can be tested independently
- Elimination of Conditionals: Replaces complex if/else or switch statements
Caveats
Consider these potential drawbacks:
- Increased Complexity: More classes/functions to manage
- Client Awareness: Clients must know about different strategies
- Communication Overhead: Strategies might need to share data
- Over-engineering: Don’t use it for simple cases with few algorithms
Thank you
The Strategy pattern is one of my favorite patterns in Go because it promotes clean, testable, and flexible code. Combined with Go’s first-class functions, it becomes even more powerful and idiomatic. Please drop an email at [email protected] if you would like to share any feedback or suggestions. Peace!