Mediator Pattern — Design Patterns

Mediator menengahi komunikasi antar-object. Daripada N object berkomunikasi langsung (N² edge), semua berkomunikasi lewat satu pusat → N edge saja. Masalah…

Mediator Pattern menaruh satu object di tengah yang mengatur komunikasi antar beberapa object, supaya mereka gak saling memanggil langsung. Tiap object cuma kenal si penengah (mediator), dan mediator yang tahu siapa harus bereaksi saat sesuatu terjadi. Bayangin menara pengatur lalu lintas udara di bandara. Pilot gak saling menelepon untuk menentukan siapa mendarat duluan. Tiap pilot cuma bicara ke menara, dan menara yang memutuskan siapa mendarat, siapa menunggu, dan landasan mana yang dipakai. Pesawat yang baru datang cukup tahu frekuensi menara, bukan nomor semua pesawat lain.

🧪 Tebakan cepat: layar dispatch Kurir Kancil punya tiga komponen yang saling memanggil langsung: daftar kurir, daftar paket, dan peta. Sekarang ditambah komponen keempat, panel ringkasan beban kurir, yang harus diperbarui tiap kali kurir atau paket dipilih. Berapa komponen lama yang harus diubah? Dan berapa kalau ketiganya bicara lewat mediator? Tebak dulu, jawabannya ada di bagian "Panel keempat: siapa yang harus diubah".

Masalahnya: komponen yang saling kenal

Di layar dispatch, memilih kurir Budi harus menyaring daftar paket ke paket milik Budi dan menggambar rutenya di peta. Memilih paket, di daftar atau lewat pin di peta, harus menyorot kurirnya. Versi pertama menulis semua itu di method klik tiap komponen:

// bagian klik dari ketiga komponen
const daftarKurir = {
  klik(nama) {
    this.sorot(nama)
    daftarPaket.saring(nama) // kenal daftarPaket
    peta.rute(nama)          // kenal peta
  },
}
const daftarPaket = {
  klik(resi) {
    const kurir = cari(resi).kurir
    daftarKurir.sorot(kurir) // kenal daftarKurir
    this.saring(kurir)
    this.sorot(resi)
    peta.rute(kurir)         // kenal peta
    peta.tandai(resi)
  },
}
const peta = {
  klik(resi) {
    const kurir = cari(resi).kurir
    daftarKurir.sorot(kurir) // kenal daftarKurir
    daftarPaket.saring(kurir) // kenal daftarPaket
    daftarPaket.sorot(resi)
    this.rute(kurir)
    this.tandai(resi)
  },
}

Tiap komponen kenal dua yang lain: nama object-nya dan nama method-nya. Itu 6 jalur panggilan untuk 3 komponen. Aturan "pilih paket berarti pindah ke kurirnya" ditulis dua kali, di daftarPaket dan di peta. Daftar paket juga gak bisa dites atau dipakai di layar lain tanpa membawa daftarKurir dan peta.

Mediator: satu pusat yang mengatur

const PAKET = [
  { resi: "KCL-0001", kurir: "Andi", kg: 2, area: "Cibiru" },
  { resi: "KCL-0002", kurir: "Budi", kg: 1, area: "Dago" },
  { resi: "KCL-0003", kurir: "Andi", kg: 4, area: "Antapani" },
  { resi: "KCL-0004", kurir: "Citra", kg: 3, area: "Buahbatu" },
  { resi: "KCL-0005", kurir: "Budi", kg: 5, area: "Setiabudi" },
  { resi: "KCL-0006", kurir: "Citra", kg: 2, area: "Arcamanik" },
]
const milik = (nama) => PAKET.filter((p) => p.kurir === nama)
const cari = (resi) => PAKET.find((p) => p.resi === resi)

function createDispatch() {
  const parts = {}
  function pilihKurir(nama) {
    parts.kurir.sorot(nama)
    parts.paket.saring(nama)
    parts.peta.rute(nama)
  }
  return {
    register(nama, part) {
      parts[nama] = part
      part.dispatch = this
    },
    notify(dari, event, data) {
      console.log("dispatch ← " + dari + ": " + event + " " + data)
      if (event === "kurirDipilih") pilihKurir(data)
      if (event === "paketDipilih") {
        pilihKurir(cari(data).kurir)
        parts.paket.sorot(data)
        parts.peta.tandai(data)
      }
    },
  }
}

const daftarKurir = {
  klik(nama) { this.dispatch.notify("daftarKurir", "kurirDipilih", nama) },
  sorot(nama) { console.log("daftarKurir: sorot " + nama) },
}
const daftarPaket = {
  klik(resi) { this.dispatch.notify("daftarPaket", "paketDipilih", resi) },
  saring(nama) { console.log("daftarPaket: tampilkan milik " + nama + ": " + milik(nama).map((p) => p.resi).join(", ")) },
  sorot(resi) { console.log("daftarPaket: sorot " + resi) },
}
const peta = {
  klik(resi) { this.dispatch.notify("peta", "paketDipilih", resi) },
  rute(nama) { console.log("peta: rute " + nama + " lewat " + milik(nama).map((p) => p.area).join(", ")) },
  tandai(resi) { console.log("peta: tandai " + resi) },
}

const dispatch = createDispatch()
dispatch.register("kurir", daftarKurir)
dispatch.register("paket", daftarPaket)
dispatch.register("peta", peta)

Satu klik, ditelusuri

daftarPaket.klik("KCL-0004")
// dispatch ← daftarPaket: paketDipilih KCL-0004
// daftarKurir: sorot Citra
// daftarPaket: tampilkan milik Citra: KCL-0004, KCL-0006
// peta: rute Citra lewat Buahbatu, Arcamanik
// daftarPaket: sorot KCL-0004
// peta: tandai KCL-0004

Daftar paket memanggil satu method, notify. Mediator mencari kurir pemilik KCL-0004 (Citra), lalu menyuruh ketiga komponen bereaksi. Hasil di layar sama persis dengan versi saling panggil. Yang berubah cuma siapa yang tahu aturannya.

Panel keempat: siapa yang harus diubah

Di versi saling panggil, panel ringkasan berarti menambah ringkasan.hitung(...) di klik milik daftarKurir, daftarPaket, dan peta: 3 komponen lama diubah, dan jalurnya naik dari 6 jadi 9. Di versi mediator, 0 komponen lama diubah: cukup dispatch.register("ringkasan", ringkasan) dan satu baris parts.ringkasan.hitung(nama) di pilihKurir. Itu jawaban tebakannya.

Kalau tiap komponen bisa mengabari semua yang lain, jumlah jalurnya n × (n − 1): 3 komponen 6 jalur, 4 komponen 12, dan 10 komponen 90. Lewat mediator, tiap komponen cuma kenal satu: 10 komponen, 10 jalur.

Mediator atau Pub/Sub?

Keduanya punya pusat di tengah, tapi pusatnya beda peran:

Mediator Pub/Sub
pusatnya tahu semua komponen dan aturan koordinasinya broker yang cuma meneruskan pesan per topik
logika di mediator ("pilih paket berarti sorot kurirnya") di tiap subscriber; broker gak punya logika
pengirim tahu mediator-nya gak tahu siapa yang subscribe
contoh layar dispatch, form checkout, dialog gudang yang mengabarkan "paket transit" ke layanan lain

Store seperti Redux, Zustand, atau Pinia juga bekerja seperti mediator: komponen mengirim action ke store, dan store yang memperbarui state lalu memberi tahu komponen yang perlu.

Mediator yang terlalu gemuk

Mediator mengumpulkan aturan di satu tempat, dan itu juga risikonya. Mediator untuk seluruh aplikasi yang berisi ribuan baris dan tahu detail tiap komponen (class CSS-nya, elemen di dalamnya) disebut God class: tiap fitur baru mengubah file yang sama, dan satu kesalahan bisa merusak semuanya. Pecah per kebutuhan, misalnya satu mediator untuk layar dispatch dan satu untuk form checkout, dan suruh komponen lewat method yang namanya menyebut maksudnya (sorot, saring), bukan dengan mengutak-atik isinya.

Latihan: form dengan mediator

Latihan nanti memakai versi yang lebih sederhana untuk form. createFormMediator() mengembalikan object dengan lima method, dan mediator-nya mengirim tiap kejadian ke semua handler yang terdaftar. Aturannya ada di handler.

Starter mendaftarkan handler validator yang mencatat invalid atau valid untuk isian email (ada @ atau gak) dan submitted: plus jumlah isian saat submit. Console-nya jadi invalid, valid, [email protected], Budi, submitted:2, dan 4 (tiga setField dan satu submit). Yang juga dinilai: dua handler yang terdaftar sama-sama menerima satu event, dan handler bernama kota menerima fieldChanged lalu submit.

⚠️ Jebakan yang sering ketemu

TL;DR: Mediator menaruh satu pusat di tengah: komponen cuma melapor ke mediator (this.dispatch.notify("daftarPaket", "paketDipilih", resi)), dan mediator yang tahu siapa harus bereaksi. Tanpa mediator, n komponen yang saling kenal bisa punya n × (n − 1) jalur (10 komponen, 90 jalur); dengan mediator, n jalur. Panel keempat di layar dispatch mengubah 3 komponen lama di versi saling panggil, dan 0 di versi mediator. Bedanya dengan Pub/Sub: mediator tahu komponennya dan berisi aturan, broker cuma meneruskan pesan. Jangan biarkan mediator jadi God class, dan jangan biarkan method yang dipanggil mediator mengabarinya lagi. Di latihan: setField dan submit mencatat event lalu memanggil semua handler, dan log() mengembalikan semua event secara berurutan.

Yang akan kamu pelajari