Aplikasi cuaca yang sedang kita kembangkan ingin data dalam bentuk objek Cuaca, tetapi penyedia datanya dua sistem tua: satu layanan internal hanya bisa mengeluarkan string CSV, satu pusat data eksternal bicara format kunci-nilai. Kode modern tidak boleh dipenuhi parsing yang tersebar hanya karena sumber datanya beragam. Adapter pattern menjawab keadaan ini dengan satu class penengah yang mengimplementasikan antarmuka yang diinginkan sambil membungkus antarmuka yang tersedia — penerjemah penuh dedikasi di perbatasan dua bentuk. Pola ini adalah pola paling sering dipakai di dunia integrasi nyata, tempat sistem jarang bisa memilih bentuk data mitranya.

Masalah Antarmuka yang Tidak Cocok #

Masalahnya bukan logika, melainkan bentuk. Layanan lawas punya method ambilCsv(String) yang mengembalikan "Jakarta;31;78"; kode kita ingin method cuacaKini(String) yang mengembalikan objek Cuaca dengan field bertipe. Kita bisa memaksa kode klien memanggil split(";") setiap kali, tetapi pilihan itu menyebarkan pengetahuan format lama ke semua sudut aplikasi: begitu format berubah, seluruh pemanggil ikut diperbaiki, dan pengujian domain harus ikut membawa string CSV.

Adapter memindahkan seluruh penerjemahan itu ke satu class di perbatasan. Kode di dalam perbatasan bersih bicara Cuaca; kode di luar perbatasan — layanan lawas — tetap tidak tersentuh dan tidak perlu dimodifikasi, karena justru itu syaratnya: sistem warisan sering tidak boleh diganggu.

Adapter dalam Kode #

Program berikut menunjukkan dua sumber lawas dengan dua format berbeda, keduanya dijembatani ke satu antarmuka:

import java.util.LinkedHashMap;
import java.util.Map;

public class AdaptasiCuaca {
    public static void main(String[] args) {
        PenyediaCuaca[] daftarPenyedia = {
                new AdapterCuacaLawas(new LayananCuacaLawas()),
                new AdapterCuacaPusat(new PusatDataCuaca())
        };

        // Klien hanya mengenal interface PenyediaCuaca
        for (PenyediaCuaca penyedia : daftarPenyedia) {
            System.out.println(penyedia.cuacaKini("Jakarta"));
            System.out.println(penyedia.cuacaKini("Bandung"));
        }
    }
}

// Interface baru yang diinginkan kode modern
interface PenyediaCuaca {
    Cuaca cuacaKini(String kota);
}

record Cuaca(String kota, int suhuC, int kelembapanPersen) {
    @Override
    public String toString() {
        return kota + ": " + suhuC + " derajat C, lembap " + kelembapanPersen + "%";
    }
}

// Sumber lawas 1: hasil berupa string CSV "kota;suhu;kelembapan"
class LayananCuacaLawas {
    String ambilCsv(String kota) {
        return switch (kota) {
            case "Jakarta" -> "Jakarta;31;78";
            case "Bandung" -> "Bandung;24;85";
            default -> "Tidak Diketahui;0;0";
        };
    }
}

// Sumber lawas 2: format kunci-nilai dengan pemisah '|'
class PusatDataCuaca {
    String unduh(String kota) {
        return switch (kota) {
            case "Jakarta" -> "kota=Jakarta|suhu=32|lembap=80";
            case "Bandung" -> "kota=Bandung|suhu=23|lembap=90";
            default -> "kota=Tidak Diketahui|suhu=0|lembap=0";
        };
    }
}

// Adapter 1: menerjemahkan CSV menjadi objek Cuaca
class AdapterCuacaLawas implements PenyediaCuaca {
    private final LayananCuacaLawas lawas;

    AdapterCuacaLawas(LayananCuacaLawas lawas) {
        this.lawas = lawas;
    }

    @Override
    public Cuaca cuacaKini(String kota) {
        String[] bagian = lawas.ambilCsv(kota).split(";");
        return new Cuaca(bagian[0],
                Integer.parseInt(bagian[1]),
                Integer.parseInt(bagian[2]));
    }
}

// Adapter 2: menerjemahkan format kunci-nilai menjadi objek Cuaca
class AdapterCuacaPusat implements PenyediaCuaca {
    private final PusatDataCuaca pusat;

    AdapterCuacaPusat(PusatDataCuaca pusat) {
        this.pusat = pusat;
    }

    @Override
    public Cuaca cuacaKini(String kota) {
        Map<String, String> nilai = new LinkedHashMap<>();
        for (String pasangan : pusat.unduh(kota).split("\\|")) {
            String[] kunciNilai = pasangan.split("=", 2);
            nilai.put(kunciNilai[0], kunciNilai[1]);
        }
        return new Cuaca(nilai.get("kota"),
                Integer.parseInt(nilai.get("suhu")),
                Integer.parseInt(nilai.get("lembap")));
    }
}

Output yang Dihasilkan:

text
Jakarta: 31 derajat C, lembap 78%
Bandung: 24 derajat C, lembap 85%
Jakarta: 32 derajat C, lembap 80%
Bandung: 23 derajat C, lembap 90%

Mari kita bedah struktur di balik program tersebut:

  1. Baris 20-22 (interface PenyediaCuaca): target interface — bentuk yang diinginkan kode modern. Seluruh kode di sisi modern hanya mengenal bentuk ini.
  2. Baris 32-53 (LayananCuacaLawas dan PusatDataCuaca): dua adaptee — sistem yang antarmukanya tidak cocok. Keduanya sengaja tidak diubah sedikit pun; dalam proyek nyata, keduanya mungkin kode milik tim lain atau pustaka yang tidak bisa diedit.
  3. Baris 54-69 (AdapterCuacaLawas): adapter pertama. Class ini mengimplementasikan target, menyimpan referensi ke adaptee lewat constructor, lalu di dalam cuacaKini (baris 63-65) melakukan seluruh penerjemahan split(";") dan parseInt — satu-satunya tempat di semesta program yang mengenal format CSV tersebut.
  4. Baris 71-87 (AdapterCuacaPusat): adapter kedua untuk format kunci-nilai. Terlihat pola yang berulang: antarmuka sama, terjemahan berbeda. Menambah sumber ketiga berarti menulis adapter ketiga, bukan menyentuh klien maupun adapter lain.
  5. Baris 6-15 (main): klien mengolah kedua penyedia lewat larik bertipe PenyediaCuaca — bukti bahwa perbedaan format sudah tidak bocor ke pemanggilan.

Adapter di JDK dan di Arsitektur #

Pustaka Java memakai pola ini sejak lama: InputStreamReader mengadaptasi aliran byte menjadi aliran karakter, dan Arrays.asList mengadaptasi array menjadi List tanpa menyalin datanya. Pada arsitektur besar, gagasan yang sama naik skala menjadi gateway di katalog Patterns of Enterprise Application Architecture milik Fowler, dan menjadi anti-corruption layer yang kita bahas di chapter Domain-Driven Design: lapisan penerjemah di perbatasan bounded context agar model sistem luar tidak merembes ke model milik kita.

Adapter menuntaskan kompatibilitas antar antarmuka. Pola berikutnya mempertanyakan hal yang lebih halus: bagaimana menambah perilaku — sms di atas email, pencatatan di atas keduanya — tanpa menyentuh class aslinya sama sekali? Decorator pattern membungkus jawabannya.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

AdaptasiCuaca.java

Unduh berkas
java
import java.util.LinkedHashMap;
import java.util.Map;

public class AdaptasiCuaca {
    public static void main(String[] args) {
        PenyediaCuaca[] daftarPenyedia = {
                new AdapterCuacaLawas(new LayananCuacaLawas()),
                new AdapterCuacaPusat(new PusatDataCuaca())
        };

        // Klien hanya mengenal interface PenyediaCuaca
        for (PenyediaCuaca penyedia : daftarPenyedia) {
            System.out.println(penyedia.cuacaKini("Jakarta"));
            System.out.println(penyedia.cuacaKini("Bandung"));
        }
    }
}

// Interface baru yang diinginkan kode modern
interface PenyediaCuaca {
    Cuaca cuacaKini(String kota);
}

record Cuaca(String kota, int suhuC, int kelembapanPersen) {
    @Override
    public String toString() {
        return kota + ": " + suhuC + " derajat C, lembap " + kelembapanPersen + "%";
    }
}

// Sumber lawas 1: hasil berupa string CSV "kota;suhu;kelembapan"
class LayananCuacaLawas {
    String ambilCsv(String kota) {
        return switch (kota) {
            case "Jakarta" -> "Jakarta;31;78";
            case "Bandung" -> "Bandung;24;85";
            default -> "Tidak Diketahui;0;0";
        };
    }
}

// Sumber lawas 2: format kunci-nilai dengan pemisah '|'
class PusatDataCuaca {
    String unduh(String kota) {
        return switch (kota) {
            case "Jakarta" -> "kota=Jakarta|suhu=32|lembap=80";
            case "Bandung" -> "kota=Bandung|suhu=23|lembap=90";
            default -> "kota=Tidak Diketahui|suhu=0|lembap=0";
        };
    }
}

// Adapter 1: menerjemahkan CSV menjadi objek Cuaca
class AdapterCuacaLawas implements PenyediaCuaca {
    private final LayananCuacaLawas lawas;

    AdapterCuacaLawas(LayananCuacaLawas lawas) {
        this.lawas = lawas;
    }

    @Override
    public Cuaca cuacaKini(String kota) {
        String[] bagian = lawas.ambilCsv(kota).split(";");
        return new Cuaca(bagian[0],
                Integer.parseInt(bagian[1]),
                Integer.parseInt(bagian[2]));
    }
}

// Adapter 2: menerjemahkan format kunci-nilai menjadi objek Cuaca
class AdapterCuacaPusat implements PenyediaCuaca {
    private final PusatDataCuaca pusat;

    AdapterCuacaPusat(PusatDataCuaca pusat) {
        this.pusat = pusat;
    }

    @Override
    public Cuaca cuacaKini(String kota) {
        Map<String, String> nilai = new LinkedHashMap<>();
        for (String pasangan : pusat.unduh(kota).split("\\|")) {
            String[] kunciNilai = pasangan.split("=", 2);
            nilai.put(kunciNilai[0], kunciNilai[1]);
        }
        return new Cuaca(nilai.get("kota"),
                Integer.parseInt(nilai.get("suhu")),
                Integer.parseInt(nilai.get("lembap")));
    }
}