Decorator Pattern — Design Patterns

Decorator Pattern menambahkan behavior baru ke object yang sudah ada tanpa mengubah kode aslinya. Seperti membungkus kado — kado tetap sama, tapi tampilannya…

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

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

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

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:

  1. Sebelum memanggil fn, cetak "Calling " + fn.name.
  2. Panggil fn(...args) dan simpan hasilnya.
  3. Sesudahnya, cetak "Result: " + hasil.
  4. Kembalikan hasilnya, supaya console.log(loggedAdd(3, 4)) mencetak 7.

Console-nya jadi enam baris: Calling add, Result: 7, 7, Calling multiply, Result: 30, 30.

Kapan gak perlu decorator

⚠️ Jebakan yang sering ketemu

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.

Yang akan kamu pelajari