Observer Pattern — Design Patterns

Observer Pattern memungkinkan satu object (subject) memberi tahu banyak object lain (observers) ketika ada perubahan state. Ini adalah pattern paling penting…

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

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:

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

⚠️ Jebakan yang sering ketemu

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.

Yang akan kamu pelajari