Proxy Pattern — Design Patterns

Proxy adalah surrogate (wakil) yang mengontrol akses ke object asli. Client mengira dia pegang object asli, padahal dia pegang proxy. Proxy memutuskan: apakah…

Proxy Pattern menaruh satu object wakil di depan object asli. Wakilnya dipakai dengan cara yang sama persis dengan object asli, jadi pemanggil gak tahu dia bicara dengan wakil. Wakil itu yang memutuskan: meneruskan ke object asli, menolak, menjawab dari catatan (cache), mencatat siapa yang bertanya, atau menunda pembuatan object aslinya sampai benar-benar dibutuhkan. Bayangin asisten direktur. Kamu menelepon nomor direktur, tapi asisten yang mengangkat. Dia bisa menjawab dari catatan ("jadwal rapat jam 3"), menolak penelepon tanpa janji, mencatat siapa yang menelepon, atau menyambungkan ke direktur. Nomornya tetap sama, dan kamu gak perlu tahu ada asisten.

🧪 Tebakan cepat: tabel tarif Kurir Kancil dibungkus Proxy dengan trap get yang mencetak baca tarif ... tiap kali sebuah tarif dibaca. Lalu kode menjalankan "kilat" in tarif. Berapa baris baca tarif yang muncul? Tebak dulu, jawabannya ada di bagian "Satu trap untuk satu jenis operasi".

Masalahnya: tarif yang salah diam-diam jadi NaN

const TARIF = { reguler: 10000, kilat: 18000, sameday: 25000 } // per kg

function hitungOngkir(service, kg) {
  return kg * TARIF[service]
}

console.log(hitungOngkir("ekspres", 2)) // NaN: layanan salah ketik, gak ada error
TARIF.kilat = "18rb"                    // diterima begitu saja
console.log(hitungOngkir("kilat", 2))   // NaN

TARIF dibaca di checkout, cek ongkir, dan API. Kamu ingin menolak layanan yang gak ada, menolak tarif yang bukan angka, dan mencatat tiap pembacaan, tanpa mengubah object TARIF dan tanpa mengubah semua pemanggilnya.

Proxy bawaan JavaScript: target, handler, trap

const tarif = new Proxy(TARIF, {
  get(target, prop, receiver) {
    if (!Object.hasOwn(target, prop)) throw new Error("Unknown service: " + String(prop))
    console.log("baca tarif " + String(prop))
    return Reflect.get(target, prop, receiver)
  },
  set(target, prop, value, receiver) {
    if (typeof value !== "number" || value <= 0) throw new TypeError("Tarif " + String(prop) + " harus angka positif")
    console.log("ubah tarif " + String(prop) + " jadi " + value)
    return Reflect.set(target, prop, value, receiver)
  },
})

function hitungOngkir(service, kg) {
  return kg * tarif[service] // sekarang lewat proxy
}

console.log(hitungOngkir("kilat", 2)) // baca tarif kilat, lalu 36000
hitungOngkir("ekspres", 2)            // Error: Unknown service: ekspres
tarif.kilat = "18rb"                  // TypeError: Tarif kilat harus angka positif

Satu trap untuk satu jenis operasi

Tiap operasi dasar pada object punya trap-nya sendiri. JavaScript punya 13 trap, dan yang paling sering dipakai:

operasi trap
tarif.kilat get
tarif.kilat = 20000 set
"kilat" in tarif has
delete tarif.kilat deleteProperty
Object.keys(tarif) ownKeys
fn(...), kalau target-nya function apply
new Fn(...), kalau target-nya class construct

Operasi yang trap-nya gak ditulis di handler diteruskan langsung ke target. Jadi "kilat" in tarif memakai trap has, bukan get, dan karena has gak ditulis, operasinya langsung ke TARIF dan menghasilkan true tanpa log: 0 baris baca tarif. Itu jawaban tebakannya. "ekspres" in tarif juga cuma menghasilkan false, tanpa error, karena validasinya ada di get.

Cache lewat trap apply

Kalau tarif diambil dari server, tiap pembacaan berarti satu request. Proxy dengan trap apply di depan function pengambilnya bisa mengingat jawabannya:

let request = 0
function ambilTarif(service) {
  request++
  console.log("GET /tarif/" + service)
  return { reguler: 10000, kilat: 18000, sameday: 25000 }[service]
}

const cache = new Map()
const ambilTarifCached = new Proxy(ambilTarif, {
  apply(target, thisArg, args) {
    const service = args[0]
    if (!cache.has(service)) cache.set(service, Reflect.apply(target, thisArg, args))
    return cache.get(service)
  },
})

ambilTarifCached("kilat")
ambilTarifCached("kilat")
ambilTarifCached("reguler")
console.log("request: " + request) // request: 2

Tiga panggilan, dua request: kilat kedua dijawab dari cache. Pemanggilnya tetap menulis ambilTarifCached("kilat") seperti memanggil function biasa.

Proxy tanpa Proxy API: lazy loading

Pattern-nya lebih tua dari fitur Proxy. Object biasa dengan method yang sama juga bisa jadi wakil. Contoh paling umum adalah virtual proxy: object asli yang mahal baru dibuat saat pertama kali benar-benar dipakai. Latihan nanti memintanya untuk koneksi database. Starter sudah berisi createDBConnection(), yang mencetak connecting... lalu mengembalikan object dengan query(sql) yang mengembalikan "result:" + sql. Kamu menulis createDBProxy() yang mengembalikan object dengan query(sql):

Console-nya jadi proxy created, connecting..., result:SELECT 1, lalu result:SELECT 2. connecting... muncul sekali saja, sesudah proxy created. Proxy apa pun yang dibuat dengan createDBProxy() juga harus meneruskan query lain: query("SELECT 9") mengembalikan result:SELECT 9.

Proxy, Decorator, dan Adapter

Proxy bawaan juga yang menjalankan beberapa library populer. reactive() di Vue 3 mengembalikan Proxy: trap get mencatat komponen mana membaca properti apa, dan trap set menggambar ulang komponen itu. Immer memberi draft berupa Proxy di produce(state, (draft) => { ... }): perubahan pada draft dicatat, lalu Immer membuat object baru dan object lamanya tetap utuh.

Kapan gak perlu proxy

⚠️ Jebakan yang sering ketemu

TL;DR: Proxy menaruh wakil dengan cara pakai yang sama di depan object asli, lalu mengontrol aksesnya: meneruskan, menolak, mengingat, mencatat, atau menunda. new Proxy(TARIF, handler) menangkap operasi lewat trap: get untuk baca (tolak ekspres, catat bacaan), set untuk tulis (tolak "18rb", harus return true), has untuk in, apply untuk memanggil function (cache: 3 panggilan, 2 request), dan seterusnya sampai 13 trap. Operasi tanpa trap diteruskan langsung, jadi "kilat" in tarif gak memicu get. Vue 3 reactive() dan Immer memakai Proxy. Proxy beda dari Adapter (mengubah bentuk) dan Decorator (menambah perilaku). Di latihan: virtual proxy yang membuat koneksi di query pertama, jadi proxy created tercetak sebelum connecting..., dan connecting... cuma sekali.

Yang akan kamu pelajari