Satu kebutuhan bisnis sering punya beberapa cara penyelesaian: diskon bisa dihitung per persen atau potongan nominal, data bisa diurutkan menurut harga atau menurut nama, rute bisa dipilih yang tercepat atau termurah. Kalau setiap variasi ditulis sebagai cabang if di dalam logika utama, kode tersebut tumbuh menjadi pohon keputusan yang sulit diuji. Strategy pattern memisahkan setiap algoritma ke unit kecil tersendiri, lalu membiarkan konteks menyimpan satu strategi aktif yang bisa diganti saat program berjalan. Go menawarkan dua wujud strategi — interface dan tipe fungsi — dan keduanya akan kita praktikkan di chapter ini.

Algoritma yang Berganti di Tengah Jalan #

Inti pola ini adalah pemisahan tiga peran. Konteks (Kasir) menjalankan algoritma, strategi (StrategiDiskon) mendefinisikan cara menghitung, dan implementasi konkret (diskonPersen, diskonNominal) mengisi detailnya. Konteks tidak memuat satu cabang kondisional pun: logika TotalSetelahDiskon hanya memanggil Hitung milik strategi yang sedang aktif.

Keuntungan terbesarnya adalah pengujian. Setiap strategi bisa diuji terpisah dengan kasusnya sendiri — diskonPersen{persen: 10} diuji dengan total yang menghasilkan angka bulat, diskonNominal diuji dengan total yang lebih kecil dari nominalnya. Menambah strategi baru, misalnya diskon bertingkat, tidak menyentuh konteks maupun strategi lama.

Strategi sebagai Interface #

Bentuk pertama memakai interface satu method — kontrak Hitung(total int) int. Implementasi konkret adalah struct kecil dengan parameter sebagai field, dan konteks menyimpan strategi sebagai field yang dapat diganti lewat SetStrategi:

go
type StrategiDiskon interface {
	Hitung(total int) int
}

type Kasir struct {
	strategi StrategiDiskon
}

func (k *Kasir) SetStrategi(s StrategiDiskon) { k.strategi = s }

Kode pemanggil mengganti perilaku Kasir tanpa membuat instance baru, karena method SetStrategi menukar strategi yang tersimpan di field. Strategi lama tidak dibatalkan atau dimusnahkan — strategi tersebut bisa dipasang kembali kapan saja.

Strategi sebagai Tipe Fungsi #

Go punya bentuk kedua yang lebih ringkas: fungsi adalah nilai, sehingga tipe fungsi sendiri bisa menjadi strategi. FungsiDiskon di bawah ini memiliki daya ekspresi yang sama dengan interface di atas:

go
type FungsiDiskon func(total int) int

func persen(p int) FungsiDiskon {
	return func(total int) int { return total * p / 100 }
}

Closure persen mengikat parameter p di dalam fungsi yang dikembalikan, memainkan peran yang di interface diisi field struct. Strategi berbentuk fungsi dapat disimpan di variabel, dikirim sebagai argumen, dikumpulkan dalam slice — sama seperti nilai lain.

Demo Kasir dan Comparator Standard Library #

Berkas strategy-demo.go pada bagian bawah halaman memperagakan kedua bentuk sekaligus, lalu ditutup contoh nyata dari standard library: slices.SortFunc menerima fungsi comparator sebagai strategi pengurutan. Kode lengkapnya adalah sebagai berikut:

go
package main

import (
	"cmp"
	"fmt"
	"slices"
)

// StrategiDiskon adalah bentuk strategi pertama: interface.
type StrategiDiskon interface {
	Hitung(total int) int
}

// diskonPersen memotong persentase tetap dari total belanja.
type diskonPersen struct{ persen int }

func (d diskonPersen) Hitung(total int) int {
	return total * d.persen / 100
}

// diskonNominal memotong sejumlah rupiah tetap, tidak melebihi total.
type diskonNominal struct{ nominal int }

func (d diskonNominal) Hitung(total int) int {
	return min(d.nominal, total)
}

// Kasir adalah konteks: memegang strategi aktif dan bisa menggantinya
// saat program berjalan lewat SetStrategi.
type Kasir struct {
	strategi StrategiDiskon
}

func (k *Kasir) SetStrategi(s StrategiDiskon) { k.strategi = s }

func (k *Kasir) TotalSetelahDiskon(total int) int {
	return total - k.strategi.Hitung(total)
}

// FungsiDiskon adalah bentuk kedua: strategi sebagai tipe fungsi.
type FungsiDiskon func(total int) int

// persen mengembalikan FungsiDiskon lewat closure.
func persen(p int) FungsiDiskon {
	return func(total int) int { return total * p / 100 }
}

// Barang memperagakan strategi nyata di stdlib: comparator.
type Barang struct {
	Nama  string
	Harga int
}

func main() {
	// Bentuk interface: kasir memilih strategi saat runtime.
	kasir := &Kasir{strategi: diskonPersen{persen: 10}}
	fmt.Println("persen 10%  :", kasir.TotalSetelahDiskon(250000))
	kasir.SetStrategi(diskonNominal{nominal: 30000})
	fmt.Println("nominal     :", kasir.TotalSetelahDiskon(250000))

	// Bentuk fungsi: strategi adalah nilai yang bisa disimpan.
	var f FungsiDiskon = persen(10)
	fmt.Println("fungsi 10%  :", 250000-f(250000))

	// Stdlib: slices.SortFunc menerima comparator sebagai strategi.
	barang := []Barang{
		{"Laptop", 15000000},
		{"Pensil", 3000},
		{"Buku", 45000},
	}
	slices.SortFunc(barang, func(a, b Barang) int {
		return cmp.Compare(a.Harga, b.Harga)
	})
	for _, b := range barang {
		fmt.Printf("%-8s %d\n", b.Nama, b.Harga)
	}
}

Output yang Dihasilkan:

text
persen 10%  : 225000
nominal     : 220000
fungsi 10%  : 225000
Pensil   3000
Buku     45000
Laptop   15000000

Mari kita telaah baris demi baris program di atas:

  1. Baris 10–12 (StrategiDiskon): kontrak satu method — bentuk interface dari strategi.
  2. Baris 15–26 (dua implementasi): diskonPersen memotong persentase, diskonNominal memakai min agar potongan tidak melebihi total belanja.
  3. Baris 34 (SetStrategi): pintu penggantian strategi saat runtime; konteks tidak perlu dibuat ulang.
  4. Baris 36–38 (TotalSetelahDiskon): logika konteks — hanya mengurangkan hasil Hitung; tidak ada if yang menyebut jenis diskon.
  5. Baris 41–46 (FungsiDiskon dan persen): bentuk fungsi dari strategi yang sama; closure mengikat p sebagai pengganti field struct.
  6. Baris 56–63 (main bagian pertama): kasir mulai dengan strategi persen, berganti ke nominal di tengah jalan, lalu strategi fungsi dipakai sebagai nilai biasa.
  7. Baris 71–73 (slices.SortFunc): algoritma pengurutan tetap, comparator menjadi strategi yang disuntik — pola strategy di standard library.

Kapan Fungsi, Kapan Interface #

Kedua bentuk sama sah; pilihan mengikuti bentuk kontrak. Pedoman praktisnya dirangkum tabel berikut:

AspekTipe FungsiInterface
Jumlah operasisatu signaturesatu atau beberapa method
Keterbacaan kontraklewat nama tipe dan komentarlewat nama setiap method
Menyimpan parameterlewat closurelewat field struct
Menambah operasi terkaitbuat tipe fungsi barutambah method, kontrak tetap satu
Contoh di stdlibslices.SortFunc, http.HandlerFuncsort.Interface, io.Reader

Sejarah standard library sendiri memberi pelajaran menarik: sort.Interface memakai tiga method, sedangkan penggantinya slices.SortFunc memilih satu fungsi comparator. Kontrak satu-operasi condong ke fungsi; kontrak yang kaya operasi tetap paling jelas sebagai interface.

Strategy mengganti isi dari satu perilaku. Chapter berikutnya menumpuk perilaku di atas perilaku: Decorator Pattern membungkus implementasi lama dengan lapisan-lapisan tambahan, dari notifikasi dengan percobaan ulang hingga pembungkus gzip di standard library.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

strategy-demo.go

Unduh berkas
go
package main

import (
	"cmp"
	"fmt"
	"slices"
)

// StrategiDiskon adalah bentuk strategi pertama: interface.
type StrategiDiskon interface {
	Hitung(total int) int
}

// diskonPersen memotong persentase tetap dari total belanja.
type diskonPersen struct{ persen int }

func (d diskonPersen) Hitung(total int) int {
	return total * d.persen / 100
}

// diskonNominal memotong sejumlah rupiah tetap, tidak melebihi total.
type diskonNominal struct{ nominal int }

func (d diskonNominal) Hitung(total int) int {
	return min(d.nominal, total)
}

// Kasir adalah konteks: memegang strategi aktif dan bisa menggantinya
// saat program berjalan lewat SetStrategi.
type Kasir struct {
	strategi StrategiDiskon
}

func (k *Kasir) SetStrategi(s StrategiDiskon) { k.strategi = s }

func (k *Kasir) TotalSetelahDiskon(total int) int {
	return total - k.strategi.Hitung(total)
}

// FungsiDiskon adalah bentuk kedua: strategi sebagai tipe fungsi.
type FungsiDiskon func(total int) int

// persen mengembalikan FungsiDiskon lewat closure.
func persen(p int) FungsiDiskon {
	return func(total int) int { return total * p / 100 }
}

// Barang memperagakan strategi nyata di stdlib: comparator.
type Barang struct {
	Nama  string
	Harga int
}

func main() {
	// Bentuk interface: kasir memilih strategi saat runtime.
	kasir := &Kasir{strategi: diskonPersen{persen: 10}}
	fmt.Println("persen 10%  :", kasir.TotalSetelahDiskon(250000))
	kasir.SetStrategi(diskonNominal{nominal: 30000})
	fmt.Println("nominal     :", kasir.TotalSetelahDiskon(250000))

	// Bentuk fungsi: strategi adalah nilai yang bisa disimpan.
	var f FungsiDiskon = persen(10)
	fmt.Println("fungsi 10%  :", 250000-f(250000))

	// Stdlib: slices.SortFunc menerima comparator sebagai strategi.
	barang := []Barang{
		{"Laptop", 15000000},
		{"Pensil", 3000},
		{"Buku", 45000},
	}
	slices.SortFunc(barang, func(a, b Barang) int {
		return cmp.Compare(a.Harga, b.Harga)
	})
	for _, b := range barang {
		fmt.Printf("%-8s %d\n", b.Nama, b.Harga)
	}
}