Strategy Pattern #
An OrderService starts its life simple: one shipping method, one pricing algorithm. Six months later, there are three shipping methods with different cost rules, five discount tiers based on membership, and two tax algorithms for domestic and international customers. The ProcessOrder code that used to be twenty lines is now eighty lines of nested if-else. Every time a new shipping method or discount tier is added, the developer has to go inside ProcessOrder, understand the existing conditions, and insert a new block in the right place — while praying no condition gets missed. The Strategy Pattern cuts this cycle. Each algorithm is moved into a separate struct implementing the same interface, and OrderService only calls that interface — without needing to know how many strategies exist or what differentiates them.
What Is the Strategy Pattern? #
The Strategy Pattern is a behavioral design pattern that defines a family of algorithms, wraps each one in a separate struct, and makes them interchangeable without changing the code that uses them. The object using the algorithm (called the Context) delegates execution to the Strategy object — it does not know or care which algorithm is running.
Two key properties distinguish Strategy from merely “having many functions”:
- Interchangeable — strategies can be swapped at runtime, not just at compile time
- Isolated — each strategy stands alone; changing one strategy does not affect the others
The Strategy Pattern is the cleanest answer to the question “how do I eliminate nested switch / if-else blocks that keep growing?” — and that question appears in nearly every evolving codebase.
flowchart LR
subgraph "Without Strategy"
C1[OrderService] -->|"if shipping == JNE"| A1["JNE logic"]
C1 -->|"else if shipping == JT"| A2["J&T logic"]
C1 -->|"else if shipping == GoSend"| A3["GoSend logic"]
C1 -.->|"add Anteraja?\\nmodify OrderService"| A4["???"]
end
subgraph "With Strategy"
C2[OrderService] -->|"strategy.Calculate()"| SI[ShippingStrategy\\ninterface]
SI --> B1[JNEStrategy]
SI --> B2[JTStrategy]
SI --> B3[GoSendStrategy]
SI --> B4["AnterajaStrategy\\nadded without\\nchanging OrderService"]
endThe Problem It Solves #
The Strategy Pattern solves one very specific problem: algorithms that vary and keep growing, embedded inside code that should not need to know the algorithm details.
The Problem: Runaway if-else #
// ANTI-PATTERN: all shipping algorithms embedded in OrderService
func (s *OrderService) CalculateShipping(order Order, method string) (int, error) {
switch method {
case "jne":
// JNE logic: weight × rate per kg + handling fee + zone
baseRate := 15000
weightRate := int(math.Ceil(order.WeightKg)) * 8000
zoneMultiplier := getJNEZoneMultiplier(order.DestCity)
return int(float64(baseRate+weightRate) * zoneMultiplier), nil
case "jt":
// J&T logic: different from JNE, has a weekend discount
baseRate := 12000
weightRate := int(math.Ceil(order.WeightKg)) * 7500
if time.Now().Weekday() == time.Saturday || time.Now().Weekday() == time.Sunday {
baseRate = int(float64(baseRate) * 0.9)
}
return baseRate + weightRate, nil
case "gosend":
// GoSend logic: per km, max 30km
distance := calculateDistance(order.OriginCity, order.DestCity)
if distance > 30 {
return 0, fmt.Errorf("GoSend only serves distances up to 30km")
}
return int(distance * 3500), nil
// Every new courier adds a case here — OrderService keeps bloating
default:
return 0, fmt.Errorf("unknown shipping method: %s", method)
}
}
// CORRECT: OrderService only knows the interface, not the implementation
func (s *OrderService) CalculateShipping(order Order, strategy ShippingStrategy) (int, error) {
return strategy.Calculate(order)
}
// Adding a new courier = creating a new struct implementing ShippingStrategy
// OrderService does not change at all
The Problem: Difficult Testing #
Without the Strategy Pattern, to test the JNE logic you have to initialize the entire OrderService with all its dependencies. With the Strategy Pattern, you only need to test JNEStrategy in isolation — no OrderService, no database, no other dependencies.
Three Components of the Strategy Pattern #
classDiagram
class ShippingStrategy {
<<interface>>
+Calculate(order Order) (int, error)
+Name() string
+EstimatedDays(destCity string) int
}
class JNEStrategy {
-baseRate int
-zoneRates map[string]float64
+Calculate(order Order) (int, error)
+Name() string
+EstimatedDays(destCity string) int
}
class JTStrategy {
-baseRate int
-weekendDiscount float64
+Calculate(order Order) (int, error)
+Name() string
+EstimatedDays(destCity string) int
}
class GoSendStrategy {
-maxDistanceKm float64
-ratePerKm int
+Calculate(order Order) (int, error)
+Name() string
+EstimatedDays(destCity string) int
}
class OrderService {
-shippingStrategy ShippingStrategy
+SetShipping(strategy ShippingStrategy)
+ProcessOrder(order Order) error
}
ShippingStrategy <|.. JNEStrategy
ShippingStrategy <|.. JTStrategy
ShippingStrategy <|.. GoSendStrategy
OrderService o-- ShippingStrategy : delegates to| Component | Role | In Go |
|---|---|---|
| Strategy interface | The algorithm contract every strategy must satisfy | Interface with a single method or several cohesive methods |
| Concrete Strategy | A specific algorithm implementation | Struct that implements the interface |
| Context | Uses the strategy without knowing its implementation | Struct holding a field of the Strategy interface type |
Full Implementation: Shipping Calculator #
A more realistic case study than the original example — a shipping cost system that considers weight, distance, and delivery time.
Strategy Interface #
package shipping
import "time"
// Order contains the order information needed for shipping cost calculation.
type Order struct {
ID string
WeightKg float64
OriginCity string
DestCity string
OrderedAt time.Time
IsCOD bool
}
// ShippingResult contains the shipping cost calculation result.
type ShippingResult struct {
Cost int
EstimatedDays int
ServiceName string
Notes string
}
// ShippingStrategy is the interface for all shipping cost algorithms.
// Each courier implements this interface in its own way.
type ShippingStrategy interface {
// Calculate computes the total shipping cost for the given order.
Calculate(order Order) (*ShippingResult, error)
// Name returns the service name to display to the user.
Name() string
// Supports checks whether this strategy serves the given route.
Supports(originCity, destCity string) bool
}
Concrete Strategies #
package shipping
import (
"fmt"
"math"
"time"
)
// JNEStrategy implements JNE shipping cost calculation.
// Weight-based rates with an inter-city zone multiplier.
type JNEStrategy struct {
BaseRate int
RatePerKg int
ZoneRates map[string]float64 // "JAKARTA-SURABAYA" -> multiplier
HandlingFee int
}
func NewJNEStrategy() *JNEStrategy {
return &JNEStrategy{
BaseRate: 15000,
RatePerKg: 8000,
HandlingFee: 2000,
ZoneRates: map[string]float64{
"JAKARTA-SURABAYA": 1.0,
"JAKARTA-MEDAN": 1.5,
"JAKARTA-MAKASSAR": 1.8,
"JAKARTA-BALI": 1.2,
},
}
}
func (s *JNEStrategy) Calculate(order Order) (*ShippingResult, error) {
if order.WeightKg <= 0 {
return nil, fmt.Errorf("package weight must be greater than 0 kg")
}
// Round up per kg
billableWeight := int(math.Ceil(order.WeightKg))
baseCost := s.BaseRate + (billableWeight * s.RatePerKg) + s.HandlingFee
// Apply the zone multiplier if one exists
zoneKey := fmt.Sprintf("%s-%s", order.OriginCity, order.DestCity)
multiplier := 1.0
if m, ok := s.ZoneRates[zoneKey]; ok {
multiplier = m
}
finalCost := int(float64(baseCost) * multiplier)
// Additional 2% COD fee
if order.IsCOD {
finalCost += int(float64(finalCost) * 0.02)
}
return &ShippingResult{
Cost: finalCost,
EstimatedDays: s.estimatedDays(order.DestCity),
ServiceName: s.Name(),
Notes: fmt.Sprintf("Billable weight: %d kg", billableWeight),
}, nil
}
func (s *JNEStrategy) Name() string { return "JNE Regular" }
func (s *JNEStrategy) Supports(origin, dest string) bool {
// JNE serves all of Indonesia
return origin != "" && dest != ""
}
func (s *JNEStrategy) estimatedDays(destCity string) int {
longDistanceCities := map[string]bool{
"MEDAN": true, "MAKASSAR": true, "MANADO": true, "JAYAPURA": true,
}
if longDistanceCities[destCity] {
return 5
}
return 3
}
// JTExpressStrategy implements J&T Express shipping cost calculation.
// Offers weekend discounts and slightly cheaper rates.
type JTExpressStrategy struct {
BaseRate int
RatePerKg int
WeekendDiscount float64
}
func NewJTExpressStrategy() *JTExpressStrategy {
return &JTExpressStrategy{
BaseRate: 12000,
RatePerKg: 7500,
WeekendDiscount: 0.10, // 10% weekend discount
}
}
func (s *JTExpressStrategy) Calculate(order Order) (*ShippingResult, error) {
if order.WeightKg <= 0 {
return nil, fmt.Errorf("package weight must be greater than 0 kg")
}
billableWeight := int(math.Ceil(order.WeightKg))
cost := s.BaseRate + (billableWeight * s.RatePerKg)
notes := fmt.Sprintf("Billable weight: %d kg", billableWeight)
// Weekend discount
day := order.OrderedAt.Weekday()
if day == time.Saturday || day == time.Sunday {
discount := int(float64(cost) * s.WeekendDiscount)
cost -= discount
notes += fmt.Sprintf(", weekend discount: Rp %d", discount)
}
return &ShippingResult{
Cost: cost,
EstimatedDays: 2,
ServiceName: s.Name(),
Notes: notes,
}, nil
}
func (s *JTExpressStrategy) Name() string { return "J&T Express" }
func (s *JTExpressStrategy) Supports(origin, dest string) bool {
return origin != "" && dest != ""
}
// GoSendStrategy implements GoSend (same-day) shipping cost calculation.
// Only for short distances, per-km rates.
type GoSendStrategy struct {
MaxDistanceKm float64
RatePerKm int
MinimumFee int
}
func NewGoSendStrategy() *GoSendStrategy {
return &GoSendStrategy{
MaxDistanceKm: 30,
RatePerKm: 3500,
MinimumFee: 15000,
}
}
func (s *GoSendStrategy) Calculate(order Order) (*ShippingResult, error) {
distance := calculateDistance(order.OriginCity, order.DestCity)
if distance > s.MaxDistanceKm {
return nil, fmt.Errorf("GoSend only serves distances up to %.0f km, your distance: %.1f km",
s.MaxDistanceKm, distance)
}
cost := int(distance * float64(s.RatePerKm))
if cost < s.MinimumFee {
cost = s.MinimumFee
}
return &ShippingResult{
Cost: cost,
EstimatedDays: 0, // same-day
ServiceName: s.Name(),
Notes: fmt.Sprintf("Distance: %.1f km, same-day delivery", distance),
}, nil
}
func (s *GoSendStrategy) Name() string { return "GoSend Same-Day" }
func (s *GoSendStrategy) Supports(origin, dest string) bool {
return calculateDistance(origin, dest) <= s.MaxDistanceKm
}
// calculateDistance is a helper — in a real implementation it uses the Google Maps API
func calculateDistance(origin, dest string) float64 {
distances := map[string]float64{
"JAKARTA-DEPOK": 20.0,
"JAKARTA-BEKASI": 25.0,
"JAKARTA-BOGOR": 60.0,
"JAKARTA-BANDUNG": 150.0,
}
key := fmt.Sprintf("%s-%s", origin, dest)
if d, ok := distances[key]; ok {
return d
}
return 200.0 // default: out of range
}
Context: OrderService #
package order
import (
"fmt"
"myapp/shipping"
)
// OrderService is the Context — it uses ShippingStrategy without knowing the details.
type OrderService struct {
shippingStrategy shipping.ShippingStrategy
// other dependencies...
}
func NewOrderService(strategy shipping.ShippingStrategy) *OrderService {
return &OrderService{shippingStrategy: strategy}
}
// SetShippingStrategy swaps the shipping strategy at runtime.
// This is the power of the Strategy Pattern — no need to create a new OrderService.
func (s *OrderService) SetShippingStrategy(strategy shipping.ShippingStrategy) {
s.shippingStrategy = strategy
}
// ProcessOrder processes an order using the configured strategy.
func (s *OrderService) ProcessOrder(order shipping.Order) error {
if s.shippingStrategy == nil {
return fmt.Errorf("shipping strategy not configured")
}
// Check whether the strategy serves this route
if !s.shippingStrategy.Supports(order.OriginCity, order.DestCity) {
return fmt.Errorf("service %s is not available for route %s → %s",
s.shippingStrategy.Name(), order.OriginCity, order.DestCity)
}
// Calculate shipping — does not know and does not care which algorithm is used
result, err := s.shippingStrategy.Calculate(order)
if err != nil {
return fmt.Errorf("shipping cost calculation failed: %w", err)
}
fmt.Printf("Order %s processed:\n", order.ID)
fmt.Printf(" Service: %s\n", result.ServiceName)
fmt.Printf(" Shipping cost: Rp %d\n", result.Cost)
fmt.Printf(" Estimated: %d days\n", result.EstimatedDays)
fmt.Printf(" Notes: %s\n", result.Notes)
return nil
}
// GetShippingOptions lists all available shipping options for a given route.
func (s *OrderService) GetShippingOptions(order shipping.Order, strategies []shipping.ShippingStrategy) []shipping.ShippingResult {
var options []shipping.ShippingResult
for _, strategy := range strategies {
if !strategy.Supports(order.OriginCity, order.DestCity) {
continue
}
result, err := strategy.Calculate(order)
if err != nil {
continue
}
options = append(options, *result)
}
return options
}
Usage with Swappable Strategies #
func main() {
order := shipping.Order{
ID: "ORD-001",
WeightKg: 2.3,
OriginCity: "JAKARTA",
DestCity: "SURABAYA",
OrderedAt: time.Now(),
IsCOD: false,
}
// Start with JNE
svc := order.NewOrderService(shipping.NewJNEStrategy())
_ = svc.ProcessOrder(order)
// Swap to J&T at runtime — no changes to OrderService code
svc.SetShippingStrategy(shipping.NewJTExpressStrategy())
_ = svc.ProcessOrder(order)
// List all available options
allStrategies := []shipping.ShippingStrategy{
shipping.NewJNEStrategy(),
shipping.NewJTExpressStrategy(),
shipping.NewGoSendStrategy(),
}
options := svc.GetShippingOptions(order, allStrategies)
fmt.Println("\nAll shipping options:")
for _, opt := range options {
fmt.Printf(" %s: Rp %d (%d days)\n", opt.ServiceName, opt.Cost, opt.EstimatedDays)
}
}
Functional Strategy: The Go Idiom #
In Go, the Strategy Pattern can be implemented more concisely using functions as values (first-class functions). This fits simple, stateless strategies very well.
// PricingStrategy as a function type — more concise than an interface for simple cases
type PricingStrategy func(basePrice float64, user User) float64
// Concrete strategies as functions
var (
// RegularPricing: normal price without discounts
RegularPricing PricingStrategy = func(basePrice float64, user User) float64 {
return basePrice
}
// MemberPricing: 10% discount for members
MemberPricing PricingStrategy = func(basePrice float64, user User) float64 {
return basePrice * 0.90
}
// PremiumPricing: 20% discount for premium members
PremiumPricing PricingStrategy = func(basePrice float64, user User) float64 {
return basePrice * 0.80
}
// FlashSalePricing: 30% discount, but only between 12.00-13.00
FlashSalePricing PricingStrategy = func(basePrice float64, user User) float64 {
hour := time.Now().Hour()
if hour == 12 {
return basePrice * 0.70
}
return basePrice
}
)
// PricingContext uses the function type as its strategy
type PricingContext struct {
strategy PricingStrategy
}
func NewPricingContext(strategy PricingStrategy) *PricingContext {
return &PricingContext{strategy: strategy}
}
func (c *PricingContext) CalculatePrice(basePrice float64, user User) float64 {
return c.strategy(basePrice, user)
}
// Selecting a strategy based on the user's tier
func SelectPricingStrategy(user User) PricingStrategy {
switch user.MemberTier {
case "premium":
return PremiumPricing
case "regular":
return MemberPricing
default:
return RegularPricing
}
}
// Usage
func ProcessPurchase(basePrice float64, user User) float64 {
strategy := SelectPricingStrategy(user)
ctx := NewPricingContext(strategy)
return ctx.CalculatePrice(basePrice, user)
}
Comparing the struct-based and functional approaches:
| Aspect | Struct-based Strategy | Functional Strategy |
|---|---|---|
| Best for | Strategies with state and many methods | Stateless strategies with a single operation |
| Testability | Very easy — test the struct in isolation | Easy — call the function directly |
| Composability | Needs a wrapper struct | Can be combined with func(f) func |
| Readability | Explicit and self-documenting | Concise, great for closures |
| Go idiom | More verbose | More idiomatic |
Combining Strategy with Factory #
In real applications, Strategy and Factory work together: the Factory picks the right strategy based on input, the Strategy runs the algorithm.
// ShippingStrategyFactory selects a strategy based on user preference and availability.
type ShippingStrategyFactory struct {
strategies map[string]shipping.ShippingStrategy
}
func NewShippingStrategyFactory() *ShippingStrategyFactory {
return &ShippingStrategyFactory{
strategies: map[string]shipping.ShippingStrategy{
"jne": shipping.NewJNEStrategy(),
"jt": shipping.NewJTExpressStrategy(),
"gosend": shipping.NewGoSendStrategy(),
},
}
}
// Get returns the strategy by service name.
func (f *ShippingStrategyFactory) Get(name string) (shipping.ShippingStrategy, error) {
strategy, ok := f.strategies[name]
if !ok {
return nil, fmt.Errorf("unknown shipping service %q", name)
}
return strategy, nil
}
// GetCheapest returns the cheapest strategy for a given order.
func (f *ShippingStrategyFactory) GetCheapest(order shipping.Order) (shipping.ShippingStrategy, error) {
var (
cheapestStrategy shipping.ShippingStrategy
cheapestCost = math.MaxInt64
)
for _, strategy := range f.strategies {
if !strategy.Supports(order.OriginCity, order.DestCity) {
continue
}
result, err := strategy.Calculate(order)
if err != nil {
continue
}
if result.Cost < cheapestCost {
cheapestCost = result.Cost
cheapestStrategy = strategy
}
}
if cheapestStrategy == nil {
return nil, fmt.Errorf("no shipping service available for this route")
}
return cheapestStrategy, nil
}
// Register adds a new strategy to the factory — for plugins or new couriers.
func (f *ShippingStrategyFactory) Register(name string, strategy shipping.ShippingStrategy) {
f.strategies[name] = strategy
}
Testing the Strategy Pattern #
One of the biggest advantages of the Strategy Pattern is how easy testing becomes — every strategy can be tested in isolation.
func TestJNEStrategy_Calculate_BasicCase(t *testing.T) {
strategy := shipping.NewJNEStrategy()
order := shipping.Order{
WeightKg: 2.0,
OriginCity: "JAKARTA",
DestCity: "SURABAYA",
OrderedAt: time.Now(),
}
result, err := strategy.Calculate(order)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Base: 15000 + (2 * 8000) + 2000 = 33000, zone JAKARTA-SURABAYA = 1.0
expectedCost := 33000
if result.Cost != expectedCost {
t.Errorf("expected cost %d, got %d", expectedCost, result.Cost)
}
if result.EstimatedDays != 3 {
t.Errorf("expected 3 days, got %d", result.EstimatedDays)
}
}
func TestJNEStrategy_Calculate_CODAdditionalFee(t *testing.T) {
strategy := shipping.NewJNEStrategy()
order := shipping.Order{
WeightKg: 1.0, OriginCity: "JAKARTA", DestCity: "SURABAYA",
OrderedAt: time.Now(), IsCOD: true,
}
result, err := strategy.Calculate(order)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
nonCODOrder := order
nonCODOrder.IsCOD = false
nonCODResult, _ := strategy.Calculate(nonCODOrder)
if result.Cost <= nonCODResult.Cost {
t.Error("COD order should cost more than non-COD")
}
}
func TestJTExpressStrategy_WeekendDiscount(t *testing.T) {
strategy := shipping.NewJTExpressStrategy()
// Find the nearest Saturday
saturday := time.Now()
for saturday.Weekday() != time.Saturday {
saturday = saturday.Add(24 * time.Hour)
}
weekdayOrder := shipping.Order{
WeightKg: 1.0, OriginCity: "JAKARTA", DestCity: "SURABAYA",
OrderedAt: time.Now(), // weekday
}
weekendOrder := weekdayOrder
weekendOrder.OrderedAt = saturday
weekdayResult, _ := strategy.Calculate(weekdayOrder)
weekendResult, _ := strategy.Calculate(weekendOrder)
if saturday.Weekday() == time.Saturday || saturday.Weekday() == time.Sunday {
if weekendResult.Cost >= weekdayResult.Cost {
t.Error("weekend order should be cheaper than weekday order")
}
}
}
func TestGoSendStrategy_ExceedsMaxDistance(t *testing.T) {
strategy := shipping.NewGoSendStrategy()
order := shipping.Order{
WeightKg: 1.0,
OriginCity: "JAKARTA",
DestCity: "BANDUNG", // distance 150km — exceeds the 30km max
}
_, err := strategy.Calculate(order)
if err == nil {
t.Error("expected error for distance exceeding max, got nil")
}
}
func TestOrderService_SwitchStrategy(t *testing.T) {
order := shipping.Order{
WeightKg: 2.0,
OriginCity: "JAKARTA",
DestCity: "SURABAYA",
OrderedAt: time.Now(),
}
svc := NewOrderService(shipping.NewJNEStrategy())
// Calculate with JNE
jneResult, _ := svc.GetShippingOptions(order, []shipping.ShippingStrategy{shipping.NewJNEStrategy()})
// Swap to J&T and calculate again — no need to reconstruct the service
svc.SetShippingStrategy(shipping.NewJTExpressStrategy())
jtResult, _ := svc.GetShippingOptions(order, []shipping.ShippingStrategy{shipping.NewJTExpressStrategy()})
// The two strategies produce different prices
if len(jneResult) > 0 && len(jtResult) > 0 && jneResult[0].Cost == jtResult[0].Cost {
t.Error("expected different costs for different strategies")
}
}
// MockShippingStrategy for testing OrderService in isolation
type MockShippingStrategy struct {
CalculateFn func(order shipping.Order) (*shipping.ShippingResult, error)
SupportsFn func(origin, dest string) bool
CalculateCalls int
}
func (m *MockShippingStrategy) Calculate(order shipping.Order) (*shipping.ShippingResult, error) {
m.CalculateCalls++
if m.CalculateFn != nil {
return m.CalculateFn(order)
}
return &shipping.ShippingResult{Cost: 10000, EstimatedDays: 3, ServiceName: "Mock"}, nil
}
func (m *MockShippingStrategy) Name() string { return "Mock" }
func (m *MockShippingStrategy) Supports(origin, dest string) bool {
if m.SupportsFn != nil {
return m.SupportsFn(origin, dest)
}
return true
}
Strategy Pattern vs Other Similar Patterns #
Strategy is often confused with the State Pattern because both involve swapping behavior. The difference lies in who triggers the swap and whether state transitions exist.
| Aspect | Strategy | State |
|---|---|---|
| What changes | The algorithm for one task | The object’s overall behavior |
| Who swaps | The client chooses the strategy | The object transitions between states itself |
| Do strategies know each other? | No | States often know other states for transitions |
| Example | Choose a sort algorithm, courier, discount | Order: Draft → Paid → Shipped → Delivered |
When to Use and When Not to #
USE Strategy if:
✓ There are several algorithm variations for the same goal
✓ Algorithm selection depends on runtime conditions (user tier, time, location)
✓ You find switch/if-else blocks that keep growing
✓ You want to add new algorithms without changing the Context
✓ You need to test each algorithm in isolation
AVOID Strategy if:
✗ There are only 1-2 algorithms that will not grow — a lambda or closure is simpler
✗ The algorithms are very short (1-2 lines) — the struct overhead is not worth it
✗ The Context needs to know strategy implementation details — this defeats the purpose
✗ Strategies need to communicate with each other — consider the Mediator
Strategy Review Checklist #
DESIGN:
□ The Strategy interface only contains the methods actually needed
□ Each concrete strategy focuses on one algorithm — no other logic
□ The Context contains no type-based if-else on strategies
□ Strategies can be swapped at runtime without reconstructing the Context
IMPLEMENTATION:
□ Strategies are stateless when possible — safe for reuse and concurrency
□ If a strategy has state, that state is thread-safe
□ Errors from strategies are propagated with informative context
□ Strategy.Name() or a similar identification method is available for logging
COMBINATION:
□ A Factory is used to select strategies based on conditions
□ Adding a new strategy requires no changes in the Context or Factory
□ A strategy registry is available if the strategy count will keep growing dynamically
TESTING:
□ Every concrete strategy is tested in isolation without the Context
□ The Context is tested with a mock strategy
□ Edge cases for each strategy are tested (invalid input, maximum limits, etc.)
Summary #
- Strategy separates algorithms from their users — the Context only knows the interface, never the implementation; adding a new algorithm does not change the Context at all.
- Replaces nested if-else — every
casein a growing switch is a strong candidate for becoming a separate concrete strategy.- Swappable at runtime — unlike inheritance, which locks behavior at compile time; strategies can be changed any time via
SetStrategy().- Two styles in Go: struct-based for strategies with state or many methods; functional (function types) for simple stateless strategies — both are valid.
- Natural combination with Factory — the Factory selects the right strategy based on conditions; the Strategy runs its algorithm; the two work together without knowing each other.
- Testing becomes very easy — every concrete strategy can be tested without the Context, without a database, without other dependencies; mock strategies enable isolated Context tests.
- Strategies should be stateless when possible — stateless strategies are safe for concurrent use by multiple goroutines and can be shared via a Singleton or factory pool.
- Distinguish it from the State Pattern: Strategy is chosen by the client from outside; State transitions by itself based on the object’s internal conditions.