Strategy Pattern adalah pola behavioral yang memungkinkan algoritma dipilih saat runtime. Alih-alih menulis when panjang di setiap pemanggil, kita meng-ekstrak setiap algoritma menjadi object strategy yang dapat ditukar-bebas. Kotlin menawarkan dua pendekatan: interface Strategy klasik dan function type (T) -> R yang jauh lebih ringkas. Pada chapter ini kita akan membandingkan keduanya, lalu menerapkan strategy untuk metode pembayaran (kartu kredit, e-wallet, transfer bank).

Interface Strategy vs Function Type #

AspekInterface StrategyFunction type
BoilerplateSetiap strategy butuh class terpisahCukup lambda atau val
StateBisa menyimpan state internalStateless (kecuali captured)
KomposisiMudah dikombinasikan dengan decoratorMudah dikomposisikan via andThen
Cocok untukStrategy kompleks dengan dependensiStrategy sederhana berbentuk fungsi

Pilihan praktis: jika strategy hanya berupa satu function dan tidak membutuhkan state, pakai function type. Jika strategy membutuhkan dependensi, konfigurasi, atau beberapa method, pakai interface.

Implementasi Strategy: Sorting dan Pembayaran #

kotlin
// Demo Strategy Pattern: interface Strategy + function as parameter (idiomatic Kotlin).

// === Classic Strategy Pattern (interface) ===
interface StrategiSortir<T> {
    fun sortir(items: List<T>): List<T>
}

class SortirAsc<T : Comparable<T>> : StrategiSortir<T> {
    override fun sortir(items: List<T>) = items.sorted()
}

class SortirDesc<T : Comparable<T>> : StrategiSortir<T> {
    override fun sortir(items: List<T>) = items.sortedDescending()
}

class SortirByLength : StrategiSortir<String> {
    override fun sortir(items: List<String>) = items.sortedBy { it.length }
}

class Konteks<T>(private var strategi: StrategiSortir<T>) {
    fun setStrategi(s: StrategiSortir<T>) { strategi = s }
    fun eksekusi(items: List<T>): List<T> = strategi.sortir(items)
}

// === Idiomatic Kotlin: function as parameter ===
fun <T> proses(items: List<T>, strategi: (List<T>) -> List<T>): List<T> = strategi(items)

val ascInt: (List<Int>) -> List<Int> = { it.sorted() }
val descInt: (List<Int>) -> List<Int> = { it.sortedDescending() }
val filterGenap: (List<Int>) -> List<Int> = { it.filter { x -> x % 2 == 0 } }

// === Strategy + data class (pembayaran) ===
interface StrategiPembayaran {
    fun bayar(jumlah: Double): String
}
class KartuKredit(private val nomor: String) : StrategiPembayaran {
    override fun bayar(jumlah: Double) = "Kartu $nomor: -Rp$jumlah"
}
class EWallet(private val nomor: String) : StrategiPembayaran {
    override fun bayar(jumlah: Double) = "E-Wallet $nomor: -Rp$jumlah"
}
class TransferBank(private val rekening: String) : StrategiPembayaran {
    override fun bayar(jumlah: Double) = "Transfer dari $rekening: -Rp$jumlah"
}

class Kasir(private var metode: StrategiPembayaran) {
    fun setMetode(m: StrategiPembayaran) { metode = m }
    fun proses(jumlah: Double): String = metode.bayar(jumlah)
}

fun main() {
    println("=== Demo Strategy Pattern ===")

    val data = listOf(5, 3, 8, 1, 9, 2, 7)

    println("--- 1. Classic Strategy (interface) ---")
    val ctx = Konteks(SortirAsc<Int>())
    println("Asc: ${ctx.eksekusi(data)}")
    ctx.setStrategi(SortirDesc())
    println("Desc: ${ctx.eksekusi(data)}")

    val kata = listOf("kotlin", "go", "rust", "typescript", "py")
    val ctxStr = Konteks(SortirByLength())
    println("By length: ${ctxStr.eksekusi(kata)}")

    println()
    println("--- 2. Idiomatic: function as parameter ---")
    println("Asc: ${proses(data, ascInt)}")
    println("Desc: ${proses(data, descInt)}")
    println("Filter genap: ${proses(data, filterGenap)}")
    // Inline lambda
    println("Sortir by modulo 3: ${proses(data, { it.sortedBy { x -> x % 3 } })}")
    // Trailing lambda
    println("Sortir by modulo 3 (trailing): ${proses(data) { it.sortedBy { x -> x % 3 } }}")

    println()
    println("--- 3. Strategy untuk pembayaran ---")
    val kasir = Kasir(KartuKredit("4111-xxxx-1234"))
    println(kasir.proses(150_000.0))
    kasir.setMetode(EWallet("081200001111"))
    println(kasir.proses(75_000.0))
    kasir.setMetode(TransferBank("BCA-9876543210"))
    println(kasir.proses(500_000.0))
}

Output yang Dihasilkan:

text
=== Demo Strategy Pattern ===
--- 1. Classic Strategy (interface) ---
Asc: [1, 2, 3, 5, 7, 8, 9]
Desc: [9, 8, 7, 5, 3, 2, 1]
By length: [go, py, rust, kotlin, typescript]

--- 2. Idiomatic: function as parameter ---
Asc: [1, 2, 3, 5, 7, 8, 9]
Desc: [9, 8, 7, 5, 3, 2, 1]
Filter genap: [8, 2]
Sortir by modulo 3: [3, 9, 1, 7, 5, 8, 2]
Sortir by modulo 3 (trailing): [3, 9, 1, 7, 5, 8, 2]

--- 3. Strategy untuk pembayaran ---
Kartu 4111-xxxx-1234: -Rp150000.0
E-Wallet 081200001111: -Rp75000.0
Transfer dari BCA-9876543210: -Rp500000.0

Bedah Kode Baris-per-Baris #

Mari kita amati perbandingan dua pendekatan:

  1. Baris 4-23 (interface strategy): StrategiSortir<T> adalah interface dengan satu method. Tiga class concrete (SortirAsc, SortirDesc, SortirByLength) mengimplementasikannya. Konteks menyimpan strategy dan dapat menukarnya via setStrategi.
  2. Baris 26 (fun proses): Versi function type hanya satu baris. Strategy diteruskan sebagai parameter (List<T>) -> List<T>. Tidak ada class concrete terpisah; lambda langsung berfungsi sebagai strategy.
  3. Baris 28-30 (val ascInt = { it.sorted() }): Strategy disimpan sebagai val bertipe function type. Pendekatan ini cocok bila strategi yang sama dipakai berulang dan ingin diberi nama.
  4. Baris 33-49 (StrategiPembayaran): Interface dengan method bayar. Setiap metode punya state sendiri (nomor kartu, nomor HP, rekening) sehingga cocok sebagai class.
  5. Baris 51-54 (Kasir): Konteks pembayaran. Method setMetode menukar metode pembayaran saat runtime tanpa mengubah kode Kasir. Hal ini memungkinkan pengguna memilih metode pembayaran setelah memasukkan barang ke keranjang.

Strategi Pembayaran dengan State #

Contoh StrategiPembayaran menunjukkan kapan interface tetap lebih cocok daripada function type. Setiap metode pembayaran menyimpan state (nomor kartu, nomor HP, nomor rekening) yang harus divalidasi sebelum dipakai. Membungkusnya sebagai class memungkinkan validasi di constructor dan encapsulation state. Mengganti ke function type akan membutuhkan closure yang menangkap state, yang kurang jelas daripada class bernama.

Penutup #

Strategy Pattern memungkinkan algoritma dipilih saat runtime tanpa mengubah kode pemanggil. Kotlin menawarkan dua bentuk: interface klasik untuk strategy kompleks dan function type untuk strategy sederhana. Pada chapter berikutnya kita akan membahas Observer Pattern: bagaimana StateFlow dan function-based subscriber menggantikan pola observer tradisional.

Referensi #

Referensi Lanjutan #