Beyond Rigid If-Else Logic
Traditional approaches to varying behavior use conditional statements scattered throughout code. Need different sorting algorithms? Different pricing strategies? Different attack behaviors in a game? You end up with messy switch statements and tightly coupled code.
The Strategy pattern lets you encapsulate algorithms and swap them at runtime. In Go, first-class functions make this pattern beautifully simple—no heavyweight class hierarchies required.
The Traditional Approach: Conditionals Everywhere
Here’s what happens without the Strategy pattern:
type PaymentProcessor struct {
paymentMethod string
}
func (p *PaymentProcessor) ProcessPayment(amount float64) error {
switch p.paymentMethod {
case "credit_card":
// Credit card logic
fmt.Printf("Processing $%.2f via credit card\n", amount)
// Validate card, charge, etc.
case "paypal":
// PayPal logic
fmt.Printf("Processing $%.2f via PayPal\n", amount)
// OAuth flow, API call, etc.
case "cryptocurrency":
// Crypto logic
fmt.Printf("Processing $%.2f via cryptocurrency\n", amount)
// Blockchain transaction, etc.
default:
return fmt.Errorf("unknown payment method: %s", p.paymentMethod)
}
return nil
}
// Problem: Adding new payment methods requires modifying this function
// Problem: Testing individual payment methods is difficult
// Problem: Can't swap strategies at runtime easily
Strategy Pattern with First-Class Functions
Let’s refactor using functions as strategies:
package main
import (
"fmt"
"math/rand"
"time"
)
// PaymentStrategy is a function type that processes payments
type PaymentStrategy func(amount float64) error
// Concrete strategies as functions
func CreditCardStrategy(amount float64) error {
fmt.Printf("Processing $%.2f via Credit Card\n", amount)
fmt.Println(" - Validating card number")
fmt.Println(" - Checking CVV")
fmt.Println(" - Charging card")
fmt.Println(" - Payment successful!")
return nil
}
func PayPalStrategy(amount float64) error {
fmt.Printf("Processing $%.2f via PayPal\n", amount)
fmt.Println(" - Redirecting to PayPal")
fmt.Println(" - User authorizes payment")
fmt.Println(" - Payment captured")
return nil
}
func CryptocurrencyStrategy(amount float64) error {
fmt.Printf("Processing $%.2f via Cryptocurrency\n", amount)
fmt.Println(" - Generating wallet address")
fmt.Println(" - Creating blockchain transaction")
fmt.Println(" - Waiting for confirmation")
fmt.Println(" - Transaction confirmed!")
return nil
}
func BankTransferStrategy(amount float64) error {
fmt.Printf("Processing $%.2f via Bank Transfer\n", amount)
fmt.Println(" - Validating bank account")
fmt.Println(" - Initiating ACH transfer")
fmt.Println(" - Transfer scheduled")
return nil
}
// PaymentProcessor now uses a strategy
type PaymentProcessor struct {
strategy PaymentStrategy
}
func NewPaymentProcessor(strategy PaymentStrategy) *PaymentProcessor {
return &PaymentProcessor{strategy: strategy}
}
func (p *PaymentProcessor) SetStrategy(strategy PaymentStrategy) {
p.strategy = strategy
fmt.Println("Payment strategy updated")
}
func (p *PaymentProcessor) Process(amount float64) error {
if p.strategy == nil {
return fmt.Errorf("no payment strategy set")
}
return p.strategy(amount)
}
func main() {
fmt.Println("=== Strategy Pattern Demo ===\n")
// Create processor with credit card strategy
processor := NewPaymentProcessor(CreditCardStrategy)
fmt.Println("--- Payment 1 ---")
processor.Process(99.99)
fmt.Println("\n--- Switching to PayPal ---")
processor.SetStrategy(PayPalStrategy)
processor.Process(49.99)
fmt.Println("\n--- Switching to Cryptocurrency ---")
processor.SetStrategy(CryptocurrencyStrategy)
processor.Process(199.99)
fmt.Println("\n=== Demo Complete ===")
}
Real-World Example: Combat System
Let’s build a flexible combat system with swappable attack strategies:
package main
import (
"fmt"
"math"
"math/rand"
)
// AttackStrategy defines how damage is calculated
type AttackStrategy func(baseDamage float64, target *Character) float64
// Different attack strategies
func NormalAttackStrategy(baseDamage float64, target *Character) float64 {
damage := baseDamage - target.Defense*0.5
return math.Max(1, damage)
}
func CriticalAttackStrategy(baseDamage float64, target *Character) float64 {
// Critical hits ignore 50% of defense and deal 2x damage
damage := (baseDamage * 2.0) - (target.Defense * 0.25)
return math.Max(1, damage)
}
func ArmorPiercingStrategy(baseDamage float64, target *Character) float64 {
// Ignores all armor
return baseDamage
}
func ElementalAttackStrategy(baseDamage float64, target *Character) float64 {
// Extra damage based on elemental weakness
multiplier := 1.0
if target.ElementWeakness {
multiplier = 1.5
}
damage := (baseDamage * multiplier) - (target.Defense * 0.3)
return math.Max(1, damage)
}
func LifeStealStrategy(baseDamage float64, target *Character) float64 {
// Returns damage dealt (caller can use this to heal)
damage := baseDamage - target.Defense*0.4
return math.Max(1, damage)
}
// Character with attack strategy
type Character struct {
Name string
Health float64
MaxHealth float64
Attack float64
Defense float64
ElementWeakness bool
AttackStrategy AttackStrategy
}
func NewCharacter(name string, health, attack, defense float64) *Character {
return &Character{
Name: name,
Health: health,
MaxHealth: health,
Attack: attack,
Defense: defense,
AttackStrategy: NormalAttackStrategy, // Default strategy
}
}
func (c *Character) SetAttackStrategy(strategy AttackStrategy) {
c.AttackStrategy = strategy
}
func (c *Character) AttackTarget(target *Character) {
if c.AttackStrategy == nil {
fmt.Printf("%s has no attack strategy!\n", c.Name)
return
}
damage := c.AttackStrategy(c.Attack, target)
target.TakeDamage(damage)
fmt.Printf("%s attacks %s for %.1f damage (HP: %.1f/%.1f)\n",
c.Name, target.Name, damage, target.Health, target.MaxHealth)
// Life steal heals the attacker
if c.AttackStrategy == LifeStealStrategy {
healAmount := damage * 0.3
c.Heal(healAmount)
fmt.Printf("%s life steals %.1f HP (HP: %.1f/%.1f)\n",
c.Name, healAmount, c.Health, c.MaxHealth)
}
}
func (c *Character) TakeDamage(damage float64) {
c.Health -= damage
if c.Health < 0 {
c.Health = 0
}
}
func (c *Character) Heal(amount float64) {
c.Health += amount
if c.Health > c.MaxHealth {
c.Health = c.MaxHealth
}
}
func (c *Character) IsAlive() bool {
return c.Health > 0
}
func ExampleCombatSystem() {
fmt.Println("=== Combat System with Strategy Pattern ===\n")
warrior := NewCharacter("Warrior", 100, 25, 15)
boss := NewCharacter("Dragon Boss", 300, 30, 20)
boss.ElementWeakness = true
// Round 1: Normal attack
fmt.Println("--- Round 1: Normal Attack ---")
warrior.AttackTarget(boss)
// Round 2: Switch to critical attack
fmt.Println("\n--- Round 2: Critical Strike ---")
warrior.SetAttackStrategy(CriticalAttackStrategy)
warrior.AttackTarget(boss)
// Round 3: Use armor piercing
fmt.Println("\n--- Round 3: Armor Piercing ---")
warrior.SetAttackStrategy(ArmorPiercingStrategy)
warrior.AttackTarget(boss)
// Round 4: Elemental attack
fmt.Println("\n--- Round 4: Elemental Attack ---")
warrior.SetAttackStrategy(ElementalAttackStrategy)
warrior.AttackTarget(boss)
// Boss strikes back
fmt.Println("\n--- Boss Counter-Attack ---")
boss.AttackTarget(warrior)
// Round 5: Life steal to recover
fmt.Println("\n--- Round 5: Life Steal ---")
warrior.SetAttackStrategy(LifeStealStrategy)
warrior.AttackTarget(boss)
fmt.Println("\n=== Combat Complete ===")
}
Advanced: Composing Strategies
Strategies can be composed for complex behaviors:
// StrategyComposer combines multiple strategies
func ComposeStrategies(strategies ...AttackStrategy) AttackStrategy {
return func(baseDamage float64, target *Character) float64 {
totalDamage := 0.0
for _, strategy := range strategies {
totalDamage += strategy(baseDamage, target)
}
return totalDamage / float64(len(strategies))
}
}
// Create a hybrid strategy
hybridStrategy := ComposeStrategies(
ElementalAttackStrategy,
ArmorPiercingStrategy,
)
warrior.SetAttackStrategy(hybridStrategy)
Strategy Pattern with Closures
Closures let you create parameterized strategies:
// DamageMultiplierStrategy creates strategies with custom multipliers
func DamageMultiplierStrategy(multiplier float64) AttackStrategy {
return func(baseDamage float64, target *Character) float64 {
damage := (baseDamage * multiplier) - target.Defense*0.5
return math.Max(1, damage)
}
}
// ChanceBasedStrategy has a probability of applying an effect
func ChanceBasedStrategy(baseStrategy AttackStrategy, chance float64) AttackStrategy {
return func(baseDamage float64, target *Character) float64 {
if rand.Float64() < chance {
fmt.Println(" [Proc!]")
return baseStrategy(baseDamage, target)
}
return NormalAttackStrategy(baseDamage, target)
}
}
// Usage
berserkerMode := DamageMultiplierStrategy(3.0)
luckyStrike := ChanceBasedStrategy(CriticalAttackStrategy, 0.3)
warrior.SetAttackStrategy(berserkerMode)
warrior.SetAttackStrategy(luckyStrike)
Real-World Example: Sorting Strategies
Classic strategy pattern use case:
package main
import (
"fmt"
"sort"
)
type SortStrategy func([]int) []int
func BubbleSort(data []int) []int {
arr := make([]int, len(data))
copy(arr, data)
n := len(arr)
for i := 0; i < n-1; i++ {
for j := 0; j < n-i-1; j++ {
if arr[j] > arr[j+1] {
arr[j], arr[j+1] = arr[j+1], arr[j]
}
}
}
return arr
}
func QuickSortStrategy(data []int) []int {
arr := make([]int, len(data))
copy(arr, data)
sort.Ints(arr) // Using built-in which is typically quicksort
return arr
}
func MergeSortStrategy(data []int) []int {
arr := make([]int, len(data))
copy(arr, data)
if len(arr) <= 1 {
return arr
}
mid := len(arr) / 2
left := MergeSortStrategy(arr[:mid])
right := MergeSortStrategy(arr[mid:])
return merge(left, right)
}
func merge(left, right []int) []int {
result := make([]int, 0, len(left)+len(right))
i, j := 0, 0
for i < len(left) && j < len(right) {
if left[i] <= right[j] {
result = append(result, left[i])
i++
} else {
result = append(result, right[j])
j++
}
}
result = append(result, left[i:]...)
result = append(result, right[j:]...)
return result
}
type Sorter struct {
strategy SortStrategy
}
func NewSorter(strategy SortStrategy) *Sorter {
return &Sorter{strategy: strategy}
}
func (s *Sorter) Sort(data []int) []int {
return s.strategy(data)
}
func (s *Sorter) SetStrategy(strategy SortStrategy) {
s.strategy = strategy
}
func ExampleSorting() {
data := []int{64, 34, 25, 12, 22, 11, 90}
fmt.Printf("Original: %v\n\n", data)
sorter := NewSorter(BubbleSort)
fmt.Println("Bubble Sort:")
fmt.Printf("Sorted: %v\n\n", sorter.Sort(data))
sorter.SetStrategy(QuickSortStrategy)
fmt.Println("Quick Sort:")
fmt.Printf("Sorted: %v\n\n", sorter.Sort(data))
sorter.SetStrategy(MergeSortStrategy)
fmt.Println("Merge Sort:")
fmt.Printf("Sorted: %v\n", sorter.Sort(data))
}
Strategy Selection Based on Context
Automatically choose strategies based on conditions:
func SelectAttackStrategy(attacker, target *Character) AttackStrategy {
// Low health? Use life steal
if attacker.Health < attacker.MaxHealth*0.3 {
return LifeStealStrategy
}
// Enemy has elemental weakness? Use elemental
if target.ElementWeakness {
return ElementalAttackStrategy
}
// High attack stat? Go for critical
if attacker.Attack > 30 {
return CriticalAttackStrategy
}
// Default to normal
return NormalAttackStrategy
}
// Auto-select strategy
warrior.SetAttackStrategy(SelectAttackStrategy(warrior, boss))
warrior.AttackTarget(boss)
Benefits of Strategy Pattern
- Flexibility: Swap algorithms at runtime
- Open/Closed Principle: Add new strategies without modifying existing code
- Testability: Test each strategy independently
- Composition: Combine strategies for complex behaviors
- Simplicity: No inheritance hierarchies needed in Go
When to Use Strategy Pattern
Strategy pattern shines when:
- You have multiple algorithms for the same task
- You need to switch behaviors at runtime
- You want to eliminate conditional logic
- You need to support different variations of an algorithm
- You’re building plugin systems or extensible frameworks
Comparison: OOP vs Functional Strategy
| Aspect | OOP (Java-style) | Go Functions |
|---|---|---|
| Verbosity | High (interfaces, classes) | Low (just functions) |
| Flexibility | Medium | High (closures) |
| Type Safety | Strong | Strong |
| Composition | Inheritance/delegation | Function composition |
| Boilerplate | Significant | Minimal |
Thank you
The Strategy pattern in Go leverages first-class functions to create elegant, flexible solutions. By treating algorithms as interchangeable values, you build systems that adapt to changing requirements without the complexity of traditional OOP implementations. It’s functional programming at its finest.
Please drop an email at [email protected] if you would like to share any feedback or suggestions. Peace!