Visitor Pattern — Design Patterns

Visitor memisahkan algoritma dari struktur data yang diproses. Kamu bisa menambah operasi baru tanpa mengubah class struktur data. Kapan berguna: Struktur…

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 dengan switch pada item.type, tanpa default, dan belum punya case "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

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:

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:

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

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).

Yang akan kamu pelajari