Observer Pattern dipakai saat satu object perlu memberi tahu banyak object lain bahwa sesuatu berubah. Object yang berubah (disebut subject) menyimpan daftar function yang minta dikabari (disebut observer atau listener), lalu memanggil semuanya begitu ada kejadian. Bayangin tukang sayur keliling yang punya daftar pelanggan WhatsApp. Begitu sayur segar datang, dia kirim satu pesan ke semua nomor di daftar. Dia gak perlu tahu siapa yang mau masak sop dan siapa yang mau bikin pecel. Yang mau ikut tinggal minta dimasukkan ke daftar, yang bosan minta dihapus.
🧪 Tebakan cepat: pelanggan membuka halaman lacak paket tiga kali, dan tiap kali halaman itu mendaftarkan listener baru tanpa pernah melepasnya. Status paket lalu berubah sekali. Berapa kali halaman lacak di-refresh? Tebak dulu, jawabannya ada di bagian "Lepas listener dengan off".
Masalahnya: updateStatus kenal semua pihak
Di Kurir Kancil versi pertama, updateStatus memanggil sendiri tiap pihak yang perlu tahu:
function updateStatus(parcel, status) {
parcel.status = status
sendSms(parcel)
refreshTracking(parcel)
}
Tiap ada pihak baru (WhatsApp, laporan harian, aplikasi kurir), isi updateStatus harus diubah. Bagian status jadi bergantung pada modul SMS, dan di test kamu gak bisa mengubah status tanpa ikut mengirim SMS.
Subject, listener, on, emit
Observer membalik arahnya: updateStatus cukup mengumumkan "status berubah", dan siapa pun yang mau tahu mendaftar sendiri.
class EventEmitter {
constructor() {
this.listeners = {} // { status: [fn, fn], ... }
}
on(event, callback) {
if (!this.listeners[event]) this.listeners[event] = []
this.listeners[event].push(callback)
}
emit(event, data) {
(this.listeners[event] || []).forEach((cb) => cb(data))
}
}
const tracker = new EventEmitter()
tracker.on("status", (p) => console.log("SMS: " + p.resi + " " + p.status))
tracker.on("status", (p) => console.log("Halaman lacak: " + p.status))
function updateStatus(parcel, status) {
parcel.status = status
tracker.emit("status", parcel) // gak tahu siapa yang mendengar
}
updateStatus({ resi: "KCL-0001" }, "transit")
// SMS: KCL-0001 transit
// Halaman lacak: transit
this.listenersadalah object yang kuncinya nama kejadian ("status"), dan isinya array callback untuk kejadian itu.onmembuat array kosong dulu kalau kejadian itu belum pernah didaftarkan, lalu menambahkan callback ke ujungnya.emitmemanggil tiap callback dengandatayang dikirim, satu per satu, sesuai urutan daftarnya. Semuanya selesai sebelumemitkembali.|| []adalah cadangan untuk kejadian yang belum punya pendengar. Tanpa itu,emit("scroll")gagal denganTypeError: Cannot read properties of undefined (reading 'forEach'). Cadangan yang sama berguna untuk menghitung pendengar: kejadian yang gak pernah didaftarkan punya 0 pendengar, dan panjang array-nya dibaca dengan.length.
Menambah WhatsApp sekarang cukup satu baris tracker.on("status", sendWhatsApp). Isi updateStatus gak berubah.
Lepas listener dengan off
Pendengar yang sudah gak perlu harus dilepas. off membuang callback yang sama persis dari daftar:
off(event, callback) {
this.listeners[event] = (this.listeners[event] || []).filter((cb) => cb !== callback)
}
function refresh(p) { console.log("Halaman lacak: " + p.status) }
tracker.on("status", refresh)
tracker.off("status", refresh) // function yang sama, jadi terlepas
off membandingkan function dengan !==, jadi yang dikirim harus function yang sama, bukan function lain yang isinya mirip. Karena itu callback yang mau dilepas ditulis sebagai function bernama atau disimpan di variabel. Banyak library membuat on mengembalikan function pelepasnya, misalnya store.subscribe di Redux: const stop = tracker.on(...), lalu stop() saat halaman ditutup.
Kalau halaman lacak dibuka tiga kali dan gak pernah memanggil off, ada tiga listener di daftar, dan satu perubahan status me-refresh halaman tiga kali. Itu jawaban tebakannya. Lebih parah lagi, ketiga halaman lama tetap tersimpan di memori karena daftar listener masih memegangnya. Ini disebut memory leak. Node.js mencetak peringatan MaxListenersExceededWarning begitu satu kejadian punya lebih dari 10 listener, karena biasanya itu tanda ada yang lupa dilepas.
Observer bawaan platform
Kamu sudah sering memakai pattern ini:
- Browser:
button.addEventListener("click", fn)danremoveEventListener. Object apa pun bisa jadi subject lewatEventTarget:target.dispatchEvent(new CustomEvent("status", { detail: parcel })). - Node.js:
EventEmitterdari modulnode:events, denganon,off,emit,once(dengar sekali lalu lepas), danlistenerCount(event).
Satu bedanya: kalau satu listener melempar error. Di class kita dan di EventEmitter Node, forEach berhenti di situ, listener sesudahnya gak jalan, dan error-nya terlempar keluar dari emit ke updateStatus. Di browser, EventTarget melaporkan error itu di console lalu tetap memanggil listener berikutnya.
Kapan cukup panggil langsung
- Penerimanya satu dan butuh jawaban. Validasi alamat harus mengembalikan benar atau salah.
emitgak mengembalikan hasil dari listener, jadi panggil saja function-nya. - Pihaknya sedikit dan gak pernah berubah. Dengan Observer,
updateStatusgak menunjukkan siapa yang mendengar. Kalau penerimanya cuma satu dan tetap, panggilan langsung lebih mudah ditelusuri.
⚠️ Jebakan yang sering ketemu
- Lupa
off. Listener menumpuk tiap halaman dibuka: notifikasi dobel, dan memori gak pernah dibebaskan. offdengan arrow function baru.tracker.off("status", (p) => refresh(p))gak melepas apa-apa, karena itu function baru yang belum pernah didaftarkan.- Satu listener error, sisanya diam. Di emitter buatan sendiri dan di Node, listener sesudah yang error gak jalan. Tangkap error di dalam listener yang rawan, misalnya pengirim SMS.
- Lupa cadangan
|| [].emitatau hitungan pendengar untuk kejadian yang belum pernah didaftarkan harus aman: hitungannya 0, bukan TypeError.
TL;DR: Observer Pattern dipakai saat satu object perlu memberi tahu banyak object lain tentang perubahan. Subject menyimpan this.listeners = {}, satu array callback per kejadian. on(event, callback) mendaftar (buat array dulu kalau belum ada), emit(event, data) memanggil semua callback kejadian itu dengan data, sesuai urutan daftar, dan off(event, callback) membuang function yang sama persis. Kejadian tanpa pendengar ditangani dengan || [], jadi hitungannya 0. Lupa off membuat listener menumpuk (memory leak), dan error di satu listener menghentikan sisanya, kecuali di EventTarget browser. Di platform: addEventListener di browser, EventEmitter di Node.