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)
createDispatch()membuat mediator-nya.partsmenyimpan komponen yang terdaftar, dan cuma mediator yang memegangnya.registermendaftarkan komponen dan memberinyapart.dispatch, satu-satunya object lain yang ia kenal.- Tiap
kliksekarang cuma satu baris:this.dispatch.notify(dari, event, data). Komponennya melapor apa yang terjadi ("kurirDipilih"), bukan menyuruh komponen lain. notifyberisi aturan koordinasinya. "Pilih paket berarti pindah ke kurirnya" sekarang ditulis sekali, dipilihKurir, dan dipakai untuk klik di daftar paket maupun di peta.sorot,saring,rute, dantandaicuma mengubah tampilan komponennya sendiri. Mereka gak tahu ada komponen lain.
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.
register(name, handler): simpan handler-nya, misalnya diMap. Handler dipanggil denganhandler(event, mediator).setField(name, value): simpan difields[name], buat event{ type: "fieldChanged", name, value }, simpan event-nya di catatan, lalu panggil semua handler.getField(name): kembalikanfields[name].submit(): buat event{ type: "submit", fields: { ...fields } }, simpan di catatan, lalu panggil semua handler.log(): kembalikan array semua event, urut sesuai kejadiannya.
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
- Satu panggilan langsung "sekali ini saja". Begitu
petamemanggildaftarPaketlangsung, aturan koordinasinya tersebar lagi, dan panel baru harus mengubah komponen lama. - Mediator yang jadi God class. Ribuan baris yang tahu isi tiap komponen. Pecah per layar atau per form.
- Pantulan tanpa akhir. Method yang dipanggil mediator (
sorot) ikut memanggilnotifylagi, dan program berhenti denganRangeError: Maximum call stack size exceeded. Yang mengabari mediator cuma aksi pengguna, seperti klik. - Di latihan: cuma memanggil handler yang namanya sama dengan isian. Semua handler harus dipanggil untuk tiap event, dan
submitjuga masuk catatan, jadilog().lengthjadi 4.
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.