Decorator Pattern membungkus sebuah function (atau object) dengan function baru yang cara memanggilnya sama, lalu menambah sesuatu sebelum atau sesudah yang asli dipanggil, tanpa mengubah yang asli. Bayangin paket di loket. Kardusnya tetap, tapi kamu bisa menambah lapisan: bubble wrap, lalu peti kayu, lalu stiker asuransi. Tiap lapisan menambah sesuatu, isinya tetap sama, dan lapisannya bisa dipasang atau dilepas sesuai pesanan.
🧪 Tebakan cepat: ongkir kilat 2 kg Rp 36.000. Packing kayu menambah Rp 15.000, promo memotong 10%. Apakah
withDiskon(withPackingKayu(hitungOngkir))danwithPackingKayu(withDiskon(hitungOngkir))menghasilkan harga yang sama? Tebak dulu, jawabannya ada di bagian "Urutan lapisan mengubah harga".
Masalahnya: function inti yang terus ditambah flag
hitungOngkir dipakai checkout, admin, dan API. Tiap tambahan biaya ditambahkan ke dalamnya:
function hitungOngkir(service, kg, asuransi, nilaiBarang, packingKayu, log) {
let price = PRICING[service](kg)
if (asuransi) price = price + nilaiBarang * 0.005
if (packingKayu) price = price + 15000
if (log) console.log("ongkir " + price)
return price
}
Tiap tambahan membuka function yang dipakai semua orang, dan tiap pemanggil ikut dapat parameter baru. Cara lain, menyalin function per gabungan (hitungOngkirAsuransi, hitungOngkirPacking, hitungOngkirAsuransiPacking, ...), lebih buruk: 3 tambahan berarti 2 × 2 × 2 = 8 salinan.
Decorator: function yang membungkus function
const PRICING = { reguler: (kg) => kg * 10000, kilat: (kg) => kg * 18000 }
function hitungOngkir(service, kg) {
return PRICING[service](kg)
}
function withAsuransi(fn, nilaiBarang) {
return function (...args) {
return fn(...args) + nilaiBarang * 0.005
}
}
function withPackingKayu(fn) {
return function (...args) {
return fn(...args) + 15000
}
}
const ongkir = withPackingKayu(withAsuransi(hitungOngkir, 2000000))
console.log(hitungOngkir("kilat", 2)) // 36000, yang asli gak berubah
console.log(ongkir("kilat", 2)) // 61000
- Decorator adalah function yang menerima function dan mengembalikan function baru. Function yang menerima atau mengembalikan function disebut higher-order function.
- Function barunya dipanggil dengan cara yang sama,
("kilat", 2), dan mengembalikan angka seperti aslinya. Pemanggil gak tahu ada lapisan. ...argsdi parameter mengumpulkan semua argumen jadi array["kilat", 2].fn(...args)menyebarkannya lagi ke function asli apa adanya. Jadi decorator yang sama cocok untuk function dengan berapa pun argumen.hitungOngkirgak disentuh. Pemanggil yang gak butuh tambahan tetap memakai yang asli.- Bentuk yang sama berlaku untuk object: pembungkusnya punya method yang sama dengan object asli, menambah sesuatu, lalu meneruskan panggilan ke object itu. Karena itu decorator sering dijelaskan sebagai menambah perilaku ke object saat program berjalan, tanpa mengubah class aslinya.
Jalannya ongkir("kilat", 2), dari lapisan terdalam:
| lapisan | menerima | mengembalikan |
|---|---|---|
hitungOngkir |
("kilat", 2) |
36000 |
withAsuransi |
36000 | 36000 + 2000000 × 0,005 = 46000 |
withPackingKayu |
46000 | 46000 + 15000 = 61000 |
Decorator untuk mencatat panggilan
Lapisan gak harus mengubah angka. Dia bisa menambah perilaku, misalnya mencatat:
function withLogging(fn) {
return function (...args) {
const result = fn(...args)
console.log(fn.name + "(" + args.join(", ") + ") = " + result)
return result
}
}
withLogging(hitungOngkir)("kilat", 2) // hitungOngkir(kilat, 2) = 36000
fn.nameadalah nama function aslinya. Tiap function yang ditulisfunction hitungOngkir(...)punya.nameberisi"hitungOngkir".return resultmeneruskan hasil asli. Tanpa itu, lapisan log membuat semua harga jadiundefined.console.logdengan beberapa argumen mencetaknya dipisah spasi:console.log("Calling with", [2, 3])mencetakCalling with [2, 3](Node menulisnya[ 2, 3 ]).
Urutan lapisan mengubah harga
function withDiskon(fn) {
return function (...args) {
return fn(...args) * 0.9
}
}
console.log(withDiskon(withPackingKayu(hitungOngkir))("kilat", 2)) // 45900
console.log(withPackingKayu(withDiskon(hitungOngkir))("kilat", 2)) // 47400
- Diskon di luar: (36000 + 15000) × 0,9 = 45900. Diskonnya ikut memotong biaya packing.
- Diskon di dalam: 36000 × 0,9 + 15000 = 47400. Packing dibayar penuh.
Gak sama, selisihnya Rp 1.500. Itu jawaban tebakannya. Urutan lapisan adalah aturan bisnis ("apakah promo berlaku untuk packing?"). Tulis susunannya di satu tempat dengan nama yang jelas, jangan dirakit ulang di tiap file.
Bentuk yang sama akan kamu temui lagi di middleware. Sintaks @nama di atas class atau method (di TypeScript dan Angular) juga bernama decorator. Idenya mirip, membungkus, tapi itu fitur bahasa yang lain, dan gak dibutuhkan di sini.
Latihan: withLogging
Latihan nanti memakai bentuk lapisan log, dengan format lain. Starter sudah berisi add(a, b), multiply(a, b), dan pemanggilannya. Kamu menulis withLogging(fn) yang mengembalikan function baru:
- Sebelum memanggil
fn, cetak"Calling " + fn.name. - Panggil
fn(...args)dan simpan hasilnya. - Sesudahnya, cetak
"Result: " + hasil. - Kembalikan hasilnya, supaya
console.log(loggedAdd(3, 4))mencetak7.
Console-nya jadi enam baris: Calling add, Result: 7, 7, Calling multiply, Result: 30, 30.
Kapan gak perlu decorator
- Perilaku intinya yang berubah. Tarif kilat naik ke Rp 20.000/kg? Ubah
PRICING, jangan dibungkus lapisan koreksi. - Tambahannya cuma dipakai sekali. Tulis langsung di tempat itu.
- Tumpukannya terlalu dalam. Lima lapisan berarti lima tempat untuk dicek saat harganya salah, dan urutan yang tersembunyi di tumpukan itu.
⚠️ Jebakan yang sering ketemu
- Lupa
return. Lapisannya mengembalikanundefined, danconsole.log(loggedAdd(3, 4))mencetakundefined, bukan7. - Memanggil
fn()tanpa...args. Function aslinya gak menerima argumen:add()menghasilkanNaN. - Membungkus lapisan lain lalu membaca
fn.name. Function yang dikembalikan decorator gak bernama, jadiwithLogging(withPackingKayu(hitungOngkir))mencatat nama kosong. - Urutan yang gak disengaja. Menukar dua lapisan bisa mengubah harga tanpa error.
TL;DR: Decorator adalah function yang menerima function dan mengembalikan function baru dengan cara panggil yang sama, menambah sesuatu sebelum atau sesudah memanggil yang asli (fn(...args)), lalu mengembalikan hasilnya. Function aslinya gak diubah, dan lapisannya bisa dipasang sesuai kebutuhan: withPackingKayu(withAsuransi(hitungOngkir, 2000000)) membuat 36000 jadi 61000. Urutan lapisan bisa mengubah hasil (45900 vs 47400), jadi tulis susunannya di satu tempat. Untuk latihan: cetak "Calling " + fn.name, panggil fn(...args), cetak "Result: " + hasil, lalu return hasilnya.