Visitor Pattern menaruh satu laporan (atau operasi apa pun) atas beberapa jenis object di satu object terpisah, yang disebut visitor, dengan satu method per jenis. Data yang dilaporkan gak diubah sama sekali. Bayangin pemeriksa yang berkeliling gudang Kurir Kancil. Petugas keuangan menghitung ongkir, petugas cetak membuat label, petugas muat menghitung jumlah paket. Mereka menyusuri rak yang sama, dan tiap petugas membawa lembar periksa sendiri dengan satu baris per jenis barang: dokumen, paket, kargo, karung. Petugas baru cukup membawa lembar baru. Tapi kalau jenis barang baru masuk gudang, semua lembar harus ditambah satu baris.
🧪 Tebakan cepat: jenis kiriman baru,
elektronik, masuk ke truk. Laporan ongkir ditulis sebagai function denganswitchpadaitem.type, tanpadefault, dan belum punyacase "elektronik". Berapa total ongkir truk yang tercetak? Tebak dulu, jawabannya ada di bagian "Jenis baru: harga yang dibayar Visitor".
Masalahnya: tiap laporan baru menambah method di tiap jenis
Truk Kurir Kancil membawa dokumen, paket, kargo, dan karung berisi kiriman lain. Kalau tiap laporan ditulis sebagai method di tiap jenis, class-nya terus menggemuk:
class Paket {
ongkir() { /* ... */ }
label() { /* ... */ }
jumlah() { /* ... */ }
// laporan asuransi, laporan pajak, ... tiap laporan baru: edit Dokumen, Paket, Kargo, Karung
}
Satu laporan tersebar di empat class, dan jenis kiriman yang harusnya stabil diubah tiap bulan, tiap ada laporan baru.
Visitor: satu laporan, satu object
const truk = {
type: "karung", kode: "TRUK-1", isi: [
{ type: "dokumen", resi: "KCL-0011" },
{ type: "paket", resi: "KCL-0012", kg: 2 },
{ type: "kargo", resi: "KCL-0013", kg: 4 },
{ type: "karung", kode: "KARUNG-A", isi: [
{ type: "dokumen", resi: "KCL-0014" },
{ type: "paket", resi: "KCL-0015", kg: 3 },
] },
],
}
function accept(item, visitor) {
switch (item.type) {
case "dokumen": return visitor.visitDokumen(item)
case "paket": return visitor.visitPaket(item)
case "kargo": return visitor.visitKargo(item)
case "karung": return visitor.visitKarung(item)
default: throw new Error("Jenis tak dikenal: " + item.type)
}
}
const ongkirVisitor = {
visitDokumen: () => 8000,
visitPaket: (p) => p.kg * 10000,
visitKargo: (k) => Math.max(k.kg, 10) * 6000,
visitKarung(karung) {
return karung.isi.reduce((total, item) => total + accept(item, this), 0)
},
}
console.log(accept(truk, ongkirVisitor)) // 126000
| kiriman | method yang terpanggil | ongkir |
|---|---|---|
| KCL-0011, dokumen | visitDokumen |
8000 |
| KCL-0012, paket 2 kg | visitPaket |
2 × 10000 = 20000 |
| KCL-0013, kargo 4 kg | visitKargo |
minimal 10 kg × 6000 = 60000 |
| KARUNG-A: dokumen + paket 3 kg | visitKarung, lalu isinya |
8000 + 30000 = 38000 |
| TRUK-1 | visitKarung |
126000 |
accept(item, visitor)memilih method visitor dari jenis kiriman. Dia satu-satunya tempat yang membacaitem.type.ongkirVisitorpunya satu method per jenis. Seluruh aturan ongkir ada di object ini.visitKarungmemanggilacceptlagi untuk tiap isinya, denganthis(visitor yang sama). Karung di dalam karung ikut terhitung.visitKarungditulis sebagai method biasa, bukan arrow function, karena arrow function gak punyathissendiri.- Laporan baru cukup satu object baru.
jumlahVisitordenganvisitDokumen: () => 1,visitPaket: () => 1,visitKargo: () => 1, danvisitKarungyang menjumlahkan isinya menghasilkan5, dan datatrukgak disentuh.
Double dispatch: dua pilihan dalam satu panggilan
Method mana yang jalan bergantung pada dua hal: jenis kiriman (paket atau kargo) dan laporannya (ongkir atau label). Saat program berjalan, Java dan JavaScript cuma memilih method dari satu object, yaitu object sebelum titik. Visitor versi class memakai dua panggilan supaya dua-duanya ikut menentukan, dan trik itu disebut double dispatch:
class Paket {
constructor(resi, kg) { this.resi = resi; this.kg = kg }
accept(visitor) { return visitor.visitPaket(this) } // pilihan 1: jenisnya Paket
}
new Paket("KCL-0012", 2).accept(ongkirVisitor) // pilihan 2: visitornya ongkir → 20000
paket.accept(visitor) memilih accept milik class Paket, lalu di dalamnya visitor.visitPaket(this) memilih method milik visitor yang dikirim.
Di JavaScript: switch pada type, atau map
Data kiriman di JavaScript biasanya object biasa dari JSON, misalnya { type: "paket", kg: 2 } dari API. Object seperti itu gak punya method accept, jadi function accept(item, visitor) dengan switch pada type di atas sudah mengerjakan pilihan pertama. Banyak kode JavaScript melangkah lebih jauh: satu function per laporan, dengan switch sendiri:
function ongkir(item) {
switch (item.type) {
case "dokumen": return 8000
case "paket": return item.kg * 10000
case "kargo": return Math.max(item.kg, 10) * 6000
case "karung": return item.isi.reduce((total, x) => total + ongkir(x), 0)
}
}
Manfaatnya sama: satu laporan di satu tempat, dan laporan baru gak mengubah data. Bentuk lain adalah map: const ONGKIR = { dokumen: () => 8000, paket: (p) => p.kg * 10000, ... }, lalu ONGKIR[item.type](item). Di TypeScript, bentuk yang paling umum adalah discriminated union (type Kiriman = Dokumen | Paket | Kargo, dibedakan oleh field type) dengan switch yang exhaustive: compiler menolak build kalau ada jenis yang belum punya case. Hasilnya sama dengan Visitor, dengan jauh lebih sedikit kode.
Jenis baru: harga yang dibayar Visitor
Laporan baru itu murah. Jenis baru itu mahal: tiap laporan harus diajari jenis itu. Saat { type: "elektronik", resi: "KCL-0016", kg: 1 } masuk ke truk:
- Function
ongkirdenganswitchtanpadefaultmengembalikanundefineduntuk elektronik, dan126000 + undefinedadalahNaN. Total yang tercetakNaN, tanpa error. Itu jawaban tebakannya. acceptdengandefault: throwlangsung berhenti denganError: Jenis tak dikenal: elektronik. Lebih baik: kesalahannya kelihatan.- Setelah
case "elektronik"ditambahkan diaccept, visitor yang lupa diberivisitElektronikberhenti denganTypeError: visitor.visitElektronik is not a function.
Jadi Visitor pas saat jenisnya stabil dan laporannya sering bertambah. Kalau jenis kiriman yang sering bertambah, menaruh perilaku di tiap jenis lebih mudah.
Latihan: Evaluator dan Printer
Latihan nanti memakai bentuk yang sama untuk pohon ekspresi matematika. Node-nya { type: "num", value } dan { type: "bin", op, left, right }, dengan op berupa "+" atau "*". Kamu menulis:
accept(node, visitor): panggilvisitor.visitNum(node)untuk"num",visitor.visitBin(node)untuk"bin", lalu kembalikan hasilnya.createEvaluator():visitNum(n)mengembalikann.value,visitBin(n)menghitungaccept(n.left, this)danaccept(n.right, this), lalu menjumlahkan untuk"+"atau mengalikan untuk"*".createPrinter():visitNum(n)mengembalikanString(n.value),visitBin(n)mengembalikan"(" + kiri + " " + op + " " + kanan + ")".
Untuk pohon (1 + 2) * 3, console mencetak 9, lalu ((1 + 2) * 3). Visitor depth yang sudah ada di starter mencetak 3 untuk pohon itu dan 1 untuk satu angka. Evaluator juga dicek di pohon lain: 3 * 4 harus menghasilkan 12.
⚠️ Jebakan yang sering ketemu
switchtanpadefault. Jenis baru jadiundefined, laluNaN, tanpa error. Lempar error didefault, atau biarkan TypeScript memeriksa semuacase.- Arrow function yang memakai
this.visitBin: (n) => accept(n.left, this)gak menerima visitor-nya sebagaithis. Pakai method biasa,visitBin(n) { ... }. - Lupa
return.acceptyang memanggilvisitor.visitNum(node)tanpareturnmembuat semua hasilundefined. - Spasi di Printer. Hasilnya harus
((1 + 2) * 3): satu spasi di kiri dan kanan operator, tanpa spasi di dalam kurung.
TL;DR: Visitor menaruh satu laporan atas beberapa jenis object di satu object, dengan satu method per jenis (visitDokumen, visitPaket, visitKargo, visitKarung), dan accept(item, visitor) memilih method dari jenisnya. Laporan baru cukup satu visitor baru tanpa menyentuh data (ongkir TRUK-1 = 126000, jumlah = 5). Double dispatch: method yang jalan dipilih dari jenis object dan jenis visitor sekaligus. Di JavaScript, data dari JSON biasanya dilaporkan dengan switch pada type atau map, dan di TypeScript dengan discriminated union dan switch yang exhaustive. Harganya: jenis baru harus ditambahkan ke semua visitor, dan switch tanpa default diam-diam menghasilkan NaN. Di latihan: accept memilih visitNum atau visitBin, Evaluator menghitung 9, Printer mencetak ((1 + 2) * 3).