Strategy Pattern #

Sebuah OrderService memulai hidupnya sederhana: satu metode pengiriman, satu algoritma harga. Enam bulan kemudian, ada tiga metode pengiriman dengan aturan biaya yang berbeda, lima tier diskon berdasarkan membership, dan dua algoritma pajak untuk pelanggan domestik dan internasional. Kode ProcessOrder yang tadinya dua puluh baris sekarang menjadi delapan puluh baris penuh if-else bersarang. Setiap kali ada metode pengiriman baru atau tier diskon baru, developer harus masuk ke dalam ProcessOrder, memahami kondisi yang sudah ada, dan menyisipkan blok baru di tempat yang tepat — sambil berdoa tidak ada kondisi yang terlewat. Strategy Pattern memotong siklus ini. Setiap algoritma dipindahkan ke struct terpisah yang mengimplementasikan interface yang sama, dan OrderService hanya memanggil interface itu — tanpa perlu tahu ada berapa strategi atau apa yang membedakannya.

Apa itu Strategy Pattern? #

Strategy Pattern adalah behavioral design pattern yang mendefinisikan sekumpulan algoritma, membungkus masing-masing ke dalam struct terpisah, dan membuatnya bisa saling dipertukarkan tanpa mengubah kode yang menggunakannya. Objek yang menggunakan algoritma (disebut Context) mendelegasikan eksekusi ke Strategy object — ia tidak tahu dan tidak peduli algoritma mana yang berjalan.

Dua sifat kunci yang membedakan Strategy dari sekadar “punya banyak fungsi”:

  • Dapat dipertukarkan — strategi bisa diganti saat runtime, bukan hanya saat compile-time
  • Terisolasi — setiap strategi berdiri sendiri; mengubah satu strategi tidak mempengaruhi yang lain

Strategy Pattern adalah jawaban paling bersih untuk pertanyaan “bagaimana saya menghilangkan switch / if-else bertingkat yang terus bertambah panjang?” — dan ini adalah pertanyaan yang muncul di hampir setiap codebase yang berkembang.

flowchart LR
    subgraph "Tanpa 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 -.->|"tambah Anteraja?\nubah OrderService"| A4["???"]
    end

    subgraph "Dengan Strategy"
        C2[OrderService] -->|"strategy.Calculate()"| SI[ShippingStrategy\ninterface]
        SI --> B1[JNEStrategy]
        SI --> B2[JTStrategy]
        SI --> B3[GoSendStrategy]
        SI --> B4["AnterajaStrategy\ntambah tanpa\nubah OrderService"]
    end

Masalah yang Dipecahkan #

Strategy Pattern menyelesaikan satu masalah yang sangat spesifik: algoritma yang bervariasi tapi sering bertambah, tertanam di dalam kode yang seharusnya tidak perlu tahu detail algoritma tersebut.

Masalah: if-else yang Tidak Bisa Dikendalikan #

// ANTI-PATTERN: semua algoritma pengiriman tertanam di OrderService
func (s *OrderService) CalculateShipping(order Order, method string) (int, error) {
    switch method {
    case "jne":
        // Logic JNE: berat × tarif per kg + biaya handling + zona
        baseRate := 15000
        weightRate := int(math.Ceil(order.WeightKg)) * 8000
        zoneMultiplier := getJNEZoneMultiplier(order.DestCity)
        return int(float64(baseRate+weightRate) * zoneMultiplier), nil

    case "jt":
        // Logic J&T: berbeda dari JNE, ada diskon weekend
        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":
        // Logic GoSend: per km, max 30km
        distance := calculateDistance(order.OriginCity, order.DestCity)
        if distance > 30 {
            return 0, fmt.Errorf("GoSend hanya melayani jarak hingga 30km")
        }
        return int(distance * 3500), nil

    // Setiap kurir baru menambah case di sini — OrderService terus membengkak
    default:
        return 0, fmt.Errorf("unknown shipping method: %s", method)
    }
}

// BENAR: OrderService hanya tahu interface, tidak tahu implementasi
func (s *OrderService) CalculateShipping(order Order, strategy ShippingStrategy) (int, error) {
    return strategy.Calculate(order)
}
// Tambah kurir baru = buat struct baru yang implement ShippingStrategy
// OrderService tidak berubah sama sekali

Masalah: Testing yang Sulit #

Tanpa Strategy Pattern, untuk test logika JNE kamu harus menginisialisasi seluruh OrderService dengan semua dependency-nya. Dengan Strategy Pattern, kamu hanya perlu test JNEStrategy secara terisolasi — tanpa OrderService, tanpa database, tanpa dependency lain.


Tiga Komponen 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
Komponen Peran Di Golang
Strategy interface Kontrak algoritma yang harus dipenuhi semua strategi Interface dengan method tunggal atau beberapa method yang kohesif
Concrete Strategy Implementasi algoritma spesifik Struct yang mengimplementasikan interface
Context Menggunakan strategi tanpa tahu implementasinya Struct yang menyimpan field bertipe Strategy interface

Implementasi Lengkap: Shipping Calculator #

Studi kasus yang lebih realistis dari contoh asli — sistem kalkulasi ongkos kirim yang mempertimbangkan berat, jarak, dan waktu pengiriman.

Strategy Interface #

package shipping

import "time"

// Order berisi informasi pesanan yang dibutuhkan untuk kalkulasi ongkir.
type Order struct {
    ID         string
    WeightKg   float64
    OriginCity string
    DestCity   string
    OrderedAt  time.Time
    IsCOD      bool
}

// ShippingResult berisi hasil kalkulasi ongkos kirim.
type ShippingResult struct {
    Cost          int
    EstimatedDays int
    ServiceName   string
    Notes         string
}

// ShippingStrategy adalah interface untuk semua algoritma kalkulasi ongkos kirim.
// Setiap kurir mengimplementasikan interface ini dengan caranya sendiri.
type ShippingStrategy interface {
    // Calculate menghitung total ongkos kirim untuk order yang diberikan.
    Calculate(order Order) (*ShippingResult, error)

    // Name mengembalikan nama layanan untuk ditampilkan ke user.
    Name() string

    // Supports memeriksa apakah strategi ini mendukung rute yang diberikan.
    Supports(originCity, destCity string) bool
}

Concrete Strategies #

package shipping

import (
    "fmt"
    "math"
    "time"
)

// JNEStrategy mengimplementasikan kalkulasi ongkir JNE.
// Tarif berbasis berat dengan pengganda zona antar kota.
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("berat paket harus lebih dari 0 kg")
    }

    // Pembulatan ke atas per kg
    billableWeight := int(math.Ceil(order.WeightKg))
    baseCost := s.BaseRate + (billableWeight * s.RatePerKg) + s.HandlingFee

    // Terapkan pengganda zona jika ada
    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)

    // Biaya COD tambahan 2%
    if order.IsCOD {
        finalCost += int(float64(finalCost) * 0.02)
    }

    return &ShippingResult{
        Cost:          finalCost,
        EstimatedDays: s.estimatedDays(order.DestCity),
        ServiceName:   s.Name(),
        Notes:         fmt.Sprintf("Berat tagih: %d kg", billableWeight),
    }, nil
}

func (s *JNEStrategy) Name() string { return "JNE Regular" }

func (s *JNEStrategy) Supports(origin, dest string) bool {
    // JNE mendukung seluruh 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 mengimplementasikan kalkulasi ongkir J&T Express.
// Menawarkan diskon di akhir pekan dan tarif yang sedikit lebih murah.
type JTExpressStrategy struct {
    BaseRate        int
    RatePerKg       int
    WeekendDiscount float64
}

func NewJTExpressStrategy() *JTExpressStrategy {
    return &JTExpressStrategy{
        BaseRate:        12000,
        RatePerKg:       7500,
        WeekendDiscount: 0.10, // 10% diskon weekend
    }
}

func (s *JTExpressStrategy) Calculate(order Order) (*ShippingResult, error) {
    if order.WeightKg <= 0 {
        return nil, fmt.Errorf("berat paket harus lebih dari 0 kg")
    }

    billableWeight := int(math.Ceil(order.WeightKg))
    cost := s.BaseRate + (billableWeight * s.RatePerKg)

    notes := fmt.Sprintf("Berat tagih: %d kg", billableWeight)

    // Diskon akhir pekan
    day := order.OrderedAt.Weekday()
    if day == time.Saturday || day == time.Sunday {
        discount := int(float64(cost) * s.WeekendDiscount)
        cost -= discount
        notes += fmt.Sprintf(", diskon weekend: 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 mengimplementasikan kalkulasi ongkir GoSend (same-day).
// Hanya untuk jarak dekat, tarif per km.
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 hanya melayani jarak hingga %.0f km, jarak kamu: %.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("Jarak: %.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 adalah helper — dalam implementasi nyata menggunakan 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: di luar jangkauan
}

Context: OrderService #

package order

import (
    "fmt"
    "myapp/shipping"
)

// OrderService adalah Context — menggunakan ShippingStrategy tanpa tahu detailnya.
type OrderService struct {
    shippingStrategy shipping.ShippingStrategy
    // dependency lain...
}

func NewOrderService(strategy shipping.ShippingStrategy) *OrderService {
    return &OrderService{shippingStrategy: strategy}
}

// SetShippingStrategy mengganti strategi pengiriman saat runtime.
// Ini adalah kekuatan Strategy Pattern — tidak perlu buat OrderService baru.
func (s *OrderService) SetShippingStrategy(strategy shipping.ShippingStrategy) {
    s.shippingStrategy = strategy
}

// ProcessOrder memproses pesanan menggunakan strategi yang dikonfigurasi.
func (s *OrderService) ProcessOrder(order shipping.Order) error {
    if s.shippingStrategy == nil {
        return fmt.Errorf("shipping strategy belum dikonfigurasi")
    }

    // Cek apakah strategi mendukung rute ini
    if !s.shippingStrategy.Supports(order.OriginCity, order.DestCity) {
        return fmt.Errorf("layanan %s tidak tersedia untuk rute %s → %s",
            s.shippingStrategy.Name(), order.OriginCity, order.DestCity)
    }

    // Hitung ongkir — tidak tahu dan tidak peduli algoritma yang digunakan
    result, err := s.shippingStrategy.Calculate(order)
    if err != nil {
        return fmt.Errorf("kalkulasi ongkir gagal: %w", err)
    }

    fmt.Printf("Order %s diproses:\n", order.ID)
    fmt.Printf("  Layanan: %s\n", result.ServiceName)
    fmt.Printf("  Ongkos kirim: Rp %d\n", result.Cost)
    fmt.Printf("  Estimasi: %d hari\n", result.EstimatedDays)
    fmt.Printf("  Catatan: %s\n", result.Notes)

    return nil
}

// GetShippingOptions menampilkan semua opsi pengiriman yang tersedia untuk rute tertentu.
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
}

Penggunaan dengan Strategy yang Bisa Berganti #

func main() {
    order := shipping.Order{
        ID:         "ORD-001",
        WeightKg:   2.3,
        OriginCity: "JAKARTA",
        DestCity:   "SURABAYA",
        OrderedAt:  time.Now(),
        IsCOD:      false,
    }

    // Mulai dengan JNE
    svc := order.NewOrderService(shipping.NewJNEStrategy())
    _ = svc.ProcessOrder(order)

    // Ganti strategi ke J&T saat runtime — tidak ada perubahan kode OrderService
    svc.SetShippingStrategy(shipping.NewJTExpressStrategy())
    _ = svc.ProcessOrder(order)

    // Tampilkan semua opsi yang tersedia
    allStrategies := []shipping.ShippingStrategy{
        shipping.NewJNEStrategy(),
        shipping.NewJTExpressStrategy(),
        shipping.NewGoSendStrategy(),
    }
    options := svc.GetShippingOptions(order, allStrategies)
    fmt.Println("\nSemua opsi pengiriman:")
    for _, opt := range options {
        fmt.Printf("  %s: Rp %d (%d hari)\n", opt.ServiceName, opt.Cost, opt.EstimatedDays)
    }
}

Functional Strategy: Idiom Golang #

Di Golang, Strategy Pattern bisa diimplementasikan lebih ringkas menggunakan fungsi sebagai nilai (function as first-class citizen). Ini sangat cocok untuk strategi yang sederhana dan tidak butuh state.

// PricingStrategy sebagai function type — lebih ringkas dari interface untuk kasus sederhana
type PricingStrategy func(basePrice float64, user User) float64

// Concrete strategies sebagai fungsi
var (
    // RegularPricing: harga normal tanpa diskon
    RegularPricing PricingStrategy = func(basePrice float64, user User) float64 {
        return basePrice
    }

    // MemberPricing: diskon 10% untuk member
    MemberPricing PricingStrategy = func(basePrice float64, user User) float64 {
        return basePrice * 0.90
    }

    // PremiumPricing: diskon 20% untuk premium member
    PremiumPricing PricingStrategy = func(basePrice float64, user User) float64 {
        return basePrice * 0.80
    }

    // FlashSalePricing: diskon 30%, tapi hanya pada jam 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 menggunakan function type sebagai strategi
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)
}

// Pemilihan strategi berdasarkan tier user
func SelectPricingStrategy(user User) PricingStrategy {
    switch user.MemberTier {
    case "premium":
        return PremiumPricing
    case "regular":
        return MemberPricing
    default:
        return RegularPricing
    }
}

// Penggunaan
func ProcessPurchase(basePrice float64, user User) float64 {
    strategy := SelectPricingStrategy(user)
    ctx := NewPricingContext(strategy)
    return ctx.CalculatePrice(basePrice, user)
}

Perbandingan struct-based vs functional approach:

Aspek Struct-based Strategy Functional Strategy
Cocok untuk Strategi dengan state dan banyak method Strategi stateless dengan satu operasi
Testability Sangat mudah — test struct secara terisolasi Mudah — panggil fungsi langsung
Komposabilitas Butuh struct wrapper Bisa dikombinasikan dengan func(f) func
Readability Eksplisit dan self-documenting Ringkas, cocok untuk closure
Golang idiom Lebih verbose Lebih idiomatic

Kombinasi Strategy dengan Factory #

Di aplikasi nyata, Strategy dan Factory bekerja bersama: Factory memilih strategy yang tepat berdasarkan input, Strategy menjalankan algoritma.

// ShippingStrategyFactory memilih strategi berdasarkan preferensi user dan ketersediaan.
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 mengembalikan strategi berdasarkan nama layanan.
func (f *ShippingStrategyFactory) Get(name string) (shipping.ShippingStrategy, error) {
    strategy, ok := f.strategies[name]
    if !ok {
        return nil, fmt.Errorf("layanan pengiriman %q tidak dikenal", name)
    }
    return strategy, nil
}

// GetCheapest mengembalikan strategi dengan harga terendah untuk order tertentu.
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("tidak ada layanan pengiriman yang tersedia untuk rute ini")
    }
    return cheapestStrategy, nil
}

// Register menambahkan strategi baru ke factory — untuk plugin atau kurir baru.
func (f *ShippingStrategyFactory) Register(name string, strategy shipping.ShippingStrategy) {
    f.strategies[name] = strategy
}

Testing Strategy Pattern #

Salah satu keunggulan terbesar Strategy Pattern adalah kemudahan testing — setiap strategi bisa di-test secara terisolasi.

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()

    // Cari hari Sabtu terdekat
    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", // jarak 150km — melebihi max 30km
    }

    _, 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())

    // Hitung dengan JNE
    jneResult, _ := svc.GetShippingOptions(order, []shipping.ShippingStrategy{shipping.NewJNEStrategy()})

    // Ganti ke J&T dan hitung lagi — service tidak perlu di-rekonstruksi
    svc.SetShippingStrategy(shipping.NewJTExpressStrategy())
    jtResult, _ := svc.GetShippingOptions(order, []shipping.ShippingStrategy{shipping.NewJTExpressStrategy()})

    // Dua strategi menghasilkan harga berbeda
    if len(jneResult) > 0 && len(jtResult) > 0 && jneResult[0].Cost == jtResult[0].Cost {
        t.Error("expected different costs for different strategies")
    }
}

// MockShippingStrategy untuk testing OrderService secara terisolasi
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 Kondisi Lain yang Mirip #

Strategy sering dikacaukan dengan State Pattern karena keduanya melibatkan pergantian behavior. Perbedaannya ada di siapa yang memicu pergantian dan apakah ada transisi state.

Aspek Strategy State
Yang berubah Algoritma untuk satu tugas Behavior keseluruhan objek
Siapa yang ganti Client memilih strategi Objek sendiri bertransisi antar state
Strategi tahu satu sama lain? Tidak State sering tahu state lain untuk transisi
Contoh Pilih algoritma sort, kurir, diskon Pesanan: Draft → Paid → Shipped → Delivered

Kapan Menggunakan dan Kapan Tidak #

GUNAKAN Strategy jika:
  ✓ Ada beberapa variasi algoritma untuk satu tujuan yang sama
  ✓ Pemilihan algoritma bergantung pada kondisi runtime (user tier, waktu, lokasi)
  ✓ Menemukan switch/if-else yang terus bertambah panjang
  ✓ Ingin bisa menambah algoritma baru tanpa mengubah Context
  ✓ Perlu test setiap algoritma secara terisolasi

HINDARI Strategy jika:
  ✗ Hanya ada 1–2 algoritma yang tidak akan bertambah — lambda atau closure lebih simple
  ✗ Algoritma sangat singkat (1-2 baris) — overhead struct tidak sepadan
  ✗ Context perlu tahu detail implementasi strategi — ini mengalahkan tujuannya
  ✗ Strategi perlu berkomunikasi satu sama lain — pertimbangkan Mediator

Checklist Review Strategy #

DESAIN:
  □ Strategy interface hanya berisi method yang benar-benar diperlukan
  □ Setiap concrete strategy fokus pada satu algoritma — tidak ada logika lain
  □ Context tidak berisi if-else berbasis tipe strategy
  □ Strategy bisa diganti di runtime tanpa merekonstruksi Context

IMPLEMENTASI:
  □ Strategy bersifat stateless jika memungkinkan — aman untuk reuse dan concurrency
  □ Jika strategy memiliki state, state tersebut thread-safe
  □ Error dari strategy di-propagate dengan context yang informatif
  □ Strategy.Name() atau metode identifikasi serupa tersedia untuk logging

KOMBINASI:
  □ Factory digunakan untuk memilih strategy berdasarkan kondisi
  □ Penambahan strategy baru tidak memerlukan perubahan di Context atau Factory
  □ Registry strategy tersedia jika jumlah strategy akan terus bertambah dinamis

TESTING:
  □ Setiap concrete strategy di-test secara terisolasi tanpa Context
  □ Context di-test dengan mock strategy
  □ Edge case setiap strategi di-test (input invalid, batas maksimum, dll)

Ringkasan #

  • Strategy memisahkan algoritma dari yang menggunakannya — Context hanya tahu interface, tidak pernah tahu implementasi; menambah algoritma baru tidak mengubah Context sama sekali.
  • Menggantikan if-else bertingkat — setiap case dalam switch yang terus bertambah adalah kandidat kuat untuk diubah menjadi concrete strategy terpisah.
  • Bisa diganti di runtime — berbeda dari inheritance yang mengunci perilaku saat compile-time; strategy bisa diubah kapan saja melalui SetStrategy().
  • Dua gaya di Golang: struct-based untuk strategi dengan state atau banyak method; functional (function type) untuk strategi stateless sederhana — keduanya valid.
  • Kombinasi natural dengan Factory — Factory memilih strategy yang tepat berdasarkan kondisi; Strategy menjalankan algoritmanya; keduanya bekerja bersama tanpa saling tahu.
  • Testing menjadi sangat mudah — setiap concrete strategy bisa di-test tanpa Context, tanpa database, tanpa dependency lain; mock strategy memungkinkan test Context yang terisolasi.
  • Strategy harus stateless jika bisa — strategi stateless aman digunakan oleh multiple goroutine sekaligus dan bisa di-share via Singleton atau factory pool.
  • Bedakan dari State Pattern: Strategy dipilih oleh client dari luar; State bertransisi sendiri berdasarkan kondisi internal objek.

← Sebelumnya: Proxy   Berikutnya: Observer →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact