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
casedalam 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.