Langsung ke konten
KamusNgoding
Mahir Behavioral 5 menit baca

Panduan Lengkap Visitor Pattern di C#

#design pattern #visitor #csharp #behavioral #.net

Pendahuluan

Pernahkah Anda menghadapi situasi di mana Anda perlu menambahkan fungsionalitas baru ke sekumpulan class yang sudah ada, tetapi tidak ingin mengubah class-class tersebut? Ini adalah masalah klasik yang sering dihadapi oleh developer ketika bekerja dengan struktur data yang kompleks atau hierarki objek yang dalam.

Visitor Pattern adalah salah satu behavioral design pattern yang memungkinkan Anda memisahkan algoritma dari objek yang dioperasikannya. Dengan kata lain, Anda bisa menambahkan operasi baru ke struktur kelas yang sudah ada tanpa memodifikasi kelas tersebut — cukup dengan membuat “visitor” baru.

Jika Anda sudah familiar dengan konsep dasar design patterns seperti yang dibahas di Mengenal Design Patterns: Fondasi Arsitektur Software, artikel ini akan membawa pemahaman Anda selangkah lebih jauh ke territory yang lebih advanced.


Memahami Masalah yang Diselesaikan oleh Visitor Pattern

Bayangkan Anda sedang membangun sistem penggajian untuk aplikasi HR, seperti yang digunakan perusahaan teknologi lokal. Anda punya beberapa tipe karyawan:

  • KaryawanTetap
  • KaryawanKontrak
  • KaryawanFreelance

Awalnya Anda hanya butuh fitur hitung gaji. Tetapi kemudian muncul kebutuhan baru: hitung pajak, hitung bonus akhir tahun, generate laporan, dan seterusnya.

Pendekatan naif: tambahkan method baru ke setiap class karyawan setiap kali ada kebutuhan baru.

// Pendekatan buruk — class jadi bloated
public class KaryawanTetap
{
    public decimal HitungGaji() { ... }
    public decimal HitungPajak() { ... }       // tambahan baru
    public decimal HitungBonus() { ... }        // tambahan baru
    public string GenerateLaporan() { ... }     // tambahan baru
}

Masalahnya:

  • Melanggar Open/Closed Principle — class harus terbuka untuk ekstensi, tertutup untuk modifikasi
  • Setiap tambahan fitur mengharuskan Anda menyentuh semua class karyawan
  • Class menjadi semakin besar dan sulit di-maintain

Visitor Pattern hadir untuk memecahkan masalah ini dengan elegan.


Komponen Inti dan Cara Kerja Visitor Pattern

Visitor Pattern terdiri dari empat komponen utama:

KomponenPeran
IVisitorInterface yang mendefinisikan method Visit untuk setiap tipe elemen
IVisitableInterface yang mendefinisikan method Accept(visitor) pada elemen
Concrete VisitorImplementasi nyata dari operasi yang ingin dilakukan
Concrete ElementClass yang menerima visitor melalui method Accept

Alur kerjanya:

  1. Client memanggil element.Accept(visitor)
  2. Di dalam Accept, element memanggil visitor.Visit(this)
  3. Karena parameter this memiliki tipe konkret, compiler memilih overload yang tepat saat runtime (inilah yang disebut double dispatch)

Konsep double dispatch adalah inti dari Visitor Pattern — keputusan method mana yang dipanggil bergantung pada dua tipe runtime sekaligus: tipe visitor dan tipe element.


Implementasi Step-by-Step di C#

Langkah 1: Definisikan Interface

// Interface untuk semua elemen yang bisa dikunjungi
public interface IKaryawanVisitable
{
    void Accept(IKaryawanVisitor visitor);
}

// Interface visitor — satu method per tipe elemen
public interface IKaryawanVisitor
{
    void Visit(KaryawanTetap karyawan);
    void Visit(KaryawanKontrak karyawan);
    void Visit(KaryawanFreelance karyawan);
}

Langkah 2: Buat Concrete Element

public class KaryawanTetap : IKaryawanVisitable
{
    public string Nama { get; set; }
    public decimal GajiPokok { get; set; }
    public int TahunKerja { get; set; }

    public KaryawanTetap(string nama, decimal gajiPokok, int tahunKerja)
    {
        Nama = nama;
        GajiPokok = gajiPokok;
        TahunKerja = tahunKerja;
    }

    // Double dispatch: element memanggil Visit dengan dirinya sendiri
    public void Accept(IKaryawanVisitor visitor)
    {
        visitor.Visit(this);
    }
}

public class KaryawanKontrak : IKaryawanVisitable
{
    public string Nama { get; set; }
    public decimal TarifPerBulan { get; set; }
    public int DurasiKontrakBulan { get; set; }

    public KaryawanKontrak(string nama, decimal tarifPerBulan, int durasiKontrakBulan)
    {
        Nama = nama;
        TarifPerBulan = tarifPerBulan;
        DurasiKontrakBulan = durasiKontrakBulan;
    }

    public void Accept(IKaryawanVisitor visitor)
    {
        visitor.Visit(this);
    }
}

public class KaryawanFreelance : IKaryawanVisitable
{
    public string Nama { get; set; }
    public decimal TarifPerProyek { get; set; }
    public int JumlahProyek { get; set; }

    public KaryawanFreelance(string nama, decimal tarifPerProyek, int jumlahProyek)
    {
        Nama = nama;
        TarifPerProyek = tarifPerProyek;
        JumlahProyek = jumlahProyek;
    }

    public void Accept(IKaryawanVisitor visitor)
    {
        visitor.Visit(this);
    }
}

Langkah 3: Implementasi Concrete Visitor

// Visitor pertama: menghitung gaji
public class PerhitunganGajiVisitor : IKaryawanVisitor
{
    public Dictionary<string, decimal> HasilGaji { get; } = new();

    public void Visit(KaryawanTetap karyawan)
    {
        // Karyawan tetap: gaji pokok + tunjangan senioritas 5% per tahun
        decimal tunjangan = karyawan.GajiPokok * 0.05m * karyawan.TahunKerja;
        decimal totalGaji = karyawan.GajiPokok + tunjangan;
        HasilGaji[karyawan.Nama] = totalGaji;
        Console.WriteLine($"[Tetap] {karyawan.Nama}: Rp {totalGaji:N0}");
    }

    public void Visit(KaryawanKontrak karyawan)
    {
        // Karyawan kontrak: tarif per bulan saja
        decimal totalGaji = karyawan.TarifPerBulan;
        HasilGaji[karyawan.Nama] = totalGaji;
        Console.WriteLine($"[Kontrak] {karyawan.Nama}: Rp {totalGaji:N0}");
    }

    public void Visit(KaryawanFreelance karyawan)
    {
        // Freelance: tarif per proyek × jumlah proyek
        decimal totalGaji = karyawan.TarifPerProyek * karyawan.JumlahProyek;
        HasilGaji[karyawan.Nama] = totalGaji;
        Console.WriteLine($"[Freelance] {karyawan.Nama}: Rp {totalGaji:N0}");
    }
}

// Visitor kedua: menghitung pajak — tanpa menyentuh class karyawan sama sekali!
public class PerhitunganPajakVisitor : IKaryawanVisitor
{
    private const decimal TarifPajak = 0.15m; // PPh 21 sederhana

    public void Visit(KaryawanTetap karyawan)
    {
        decimal gajiKena = karyawan.GajiPokok * (1 + 0.05m * karyawan.TahunKerja);
        decimal pajak = gajiKena * TarifPajak;
        Console.WriteLine($"[Pajak Tetap] {karyawan.Nama}: Rp {pajak:N0}");
    }

    public void Visit(KaryawanKontrak karyawan)
    {
        decimal pajak = karyawan.TarifPerBulan * TarifPajak;
        Console.WriteLine($"[Pajak Kontrak] {karyawan.Nama}: Rp {pajak:N0}");
    }

    public void Visit(KaryawanFreelance karyawan)
    {
        // Freelance biasanya kena tarif lebih tinggi
        decimal penghasilan = karyawan.TarifPerProyek * karyawan.JumlahProyek;
        decimal pajak = penghasilan * (TarifPajak + 0.05m);
        Console.WriteLine($"[Pajak Freelance] {karyawan.Nama}: Rp {pajak:N0}");
    }
}

Langkah 4: Jalankan Visitor

class Program
{
    static void Main(string[] args)
    {
        var karyawanList = new List<IKaryawanVisitable>
        {
            new KaryawanTetap("Budi", 8_000_000, 5),
            new KaryawanKontrak("Siti", 6_500_000, 12),
            new KaryawanFreelance("Andi", 3_000_000, 4)
        };

        Console.WriteLine("=== Perhitungan Gaji ===");
        var gajiVisitor = new PerhitunganGajiVisitor();
        foreach (var k in karyawanList)
            k.Accept(gajiVisitor);

        Console.WriteLine("\n=== Perhitungan Pajak ===");
        var pajakVisitor = new PerhitunganPajakVisitor();
        foreach (var k in karyawanList)
            k.Accept(pajakVisitor);
    }
}

Output:

=== Perhitungan Gaji ===
[Tetap] Budi: Rp 10.000.000
[Kontrak] Siti: Rp 6.500.000
[Freelance] Andi: Rp 12.000.000

=== Perhitungan Pajak ===
[Pajak Tetap] Budi: Rp 1.500.000
[Pajak Kontrak] Siti: Rp 975.000
[Pajak Freelance] Andi: Rp 2.400.000

Kelebihan dan Kekurangan Visitor Pattern

Kelebihan

  • Open/Closed Principle terpenuhi: Tambah operasi baru = buat visitor baru, tanpa sentuh class lama
  • Single Responsibility: Setiap visitor fokus pada satu operasi
  • Akumulasi state: Visitor bisa menyimpan hasil selama traversal (seperti HasilGaji di contoh di atas)
  • Fleksibilitas tinggi: Bisa digunakan bersama struktur pohon (Composite Pattern)

Kekurangan

  • Sulit tambah elemen baru: Jika Anda tambah KaryawanMagang, Anda harus update semua visitor yang ada
  • Mengekspos internal class: Visitor perlu akses ke data internal elemen, yang bisa melanggar enkapsulasi
  • Overkill untuk kasus sederhana: Jika hanya ada 1–2 operasi, pattern ini terlalu kompleks

Penting untuk memahami trade-off ini, seperti halnya memilih algoritma yang tepat — misalnya ketika Anda membandingkan berbagai pendekatan sorting seperti yang dibahas di Membandingkan Algoritma Sorting: Kapan Menggunakan Bubble, Merge, dan Quick Sort?.


Contoh Kasus Nyata: Laporan Rekapitulasi Penggajian

Mari kita lihat implementasi yang lebih lengkap dengan penambahan fitur laporan rekapitulasi, tanpa mengubah satu baris pun dari class karyawan:

public class LaporanRekapVisitor : IKaryawanVisitor
{
    private readonly List<string> _laporan = new();
    private decimal _totalPengeluaran = 0;

    public void Visit(KaryawanTetap karyawan)
    {
        decimal gaji = karyawan.GajiPokok * (1 + 0.05m * karyawan.TahunKerja);
        _totalPengeluaran += gaji;
        _laporan.Add($"| {karyawan.Nama,-15} | Tetap     | Rp {gaji,12:N0} |");
    }

    public void Visit(KaryawanKontrak karyawan)
    {
        _totalPengeluaran += karyawan.TarifPerBulan;
        _laporan.Add($"| {karyawan.Nama,-15} | Kontrak   | Rp {karyawan.TarifPerBulan,12:N0} |");
    }

    public void Visit(KaryawanFreelance karyawan)
    {
        decimal penghasilan = karyawan.TarifPerProyek * karyawan.JumlahProyek;
        _totalPengeluaran += penghasilan;
        _laporan.Add($"| {karyawan.Nama,-15} | Freelance | Rp {penghasilan,12:N0} |");
    }

    public void TampilkanLaporan()
    {
        Console.WriteLine("+------------------+-----------+----------------+");
        Console.WriteLine("| Nama             | Status    | Total Gaji     |");
        Console.WriteLine("+------------------+-----------+----------------+");
        foreach (var baris in _laporan)
            Console.WriteLine(baris);
        Console.WriteLine("+------------------+-----------+----------------+");
        Console.WriteLine($"| TOTAL PENGELUARAN                  Rp {_totalPengeluaran,12:N0} |");
        Console.WriteLine("+------------------+-----------+----------------+");
    }
}

Penggunaan:

var rekapVisitor = new LaporanRekapVisitor();
foreach (var k in karyawanList)
    k.Accept(rekapVisitor);

rekapVisitor.TampilkanLaporan();

Output:

+------------------+-----------+----------------+
| Nama             | Status    | Total Gaji     |
+------------------+-----------+----------------+
| Budi             | Tetap     | Rp   10.000.000 |
| Siti             | Kontrak   | Rp    6.500.000 |
| Andi             | Freelance | Rp   12.000.000 |
+------------------+-----------+----------------+
| TOTAL PENGELUARAN                  Rp   28.500.000 |
+------------------+-----------+----------------+

Perhatikan: kita berhasil menambahkan fitur laporan lengkap tanpa menyentuh class KaryawanTetap, KaryawanKontrak, maupun KaryawanFreelance sama sekali.


Pertanyaan yang Sering Diajukan

Apa itu double dispatch dan mengapa penting di Visitor Pattern?

Double dispatch adalah mekanisme di mana method yang dipanggil ditentukan oleh dua tipe runtime sekaligus — tipe objek dan tipe parameter. Di C#, method overloading biasa hanya resolusi satu dimensi (berdasarkan tipe parameter saat kompilasi). Visitor Pattern menyiasati ini dengan cara element memanggil visitor.Visit(this) di dalam Accept, sehingga tipe this yang konkret memilih overload yang tepat saat runtime.

Apa perbedaan Visitor Pattern dengan Strategy Pattern?

Strategy Pattern mengganti cara sebuah objek melakukan satu operasi, sementara Visitor Pattern memungkinkan banyak operasi berbeda dijalankan pada sekelompok objek. Strategy cocok ketika Anda ingin menukar algoritma pada satu objek; Visitor cocok ketika Anda ingin menambahkan banyak operasi ke hierarki objek yang stabil tanpa memodifikasinya.

Kapan sebaiknya tidak menggunakan Visitor Pattern?

Hindari Visitor Pattern jika hierarki element-mu sering berubah (tambah/hapus tipe). Setiap penambahan tipe baru memaksa Anda memperbarui semua visitor yang ada — ini bisa menjadi beban besar. Visitor paling cocok ketika struktur class stabil tetapi operasi terus berkembang.

Bagaimana cara Visitor Pattern bekerja dengan Composite Pattern?

Visitor dan Composite adalah kombinasi yang sangat umum. Dalam Composite, Anda punya struktur pohon (node dan leaf). Visitor bisa traversal pohon ini secara rekursif — node memanggil Accept untuk semua child-nya, lalu memanggil visitor.Visit(this). Ini memungkinkan operasi seperti kalkulasi total, serialisasi, atau rendering di seluruh pohon dilakukan hanya dengan satu visitor.

Apakah Visitor Pattern cocok untuk proyek skala kecil?

Untuk proyek kecil dengan 1–2 operasi dan hierarki class yang sederhana, Visitor Pattern cenderung overkill dan malah memperumit kode. Pattern ini paling bermanfaat ketika Anda mengantisipasi akan ada banyak operasi berbeda di masa depan pada struktur class yang relatif stabil, seperti compiler, document processor, atau sistem pelaporan yang kompleks.


Kesimpulan

Visitor Pattern adalah salah satu behavioral pattern paling powerful sekaligus paling sering disalahpahami. Kuncinya ada pada konsep double dispatch — mekanisme di mana keputusan method yang dipanggil ditentukan oleh dua tipe runtime sekaligus.

Gunakan Visitor ketika:

  • Anda punya hierarki class yang stabil dan tidak sering berubah
  • Anda perlu menambahkan banyak operasi baru ke hierarki tersebut
  • Anda ingin menjaga class-class tersebut tetap bersih dari logika yang tidak berkaitan

Di C#, implementasinya bersih dan ekspresif berkat dukungan method overloading dan interface yang kuat. Selamat belajar dan terus bereksperimen — setiap pattern yang Anda kuasai adalah satu senjata baru dalam arsenal arsitektur softwaremu, dan KamusNgoding selalu ada untuk menemani perjalanan belajarmu!

Artikel Terkait