Strategy memang bekerja mulus selama algoritma berada di tangan kita sendiri — kita merancang kontraknya dan menulis implementasinya. Dunia nyata jarang sebaik itu: aplikasi harus berurusan dengan gerbang pembayaran warisan yang ditulis bertahun-tahun lalu, atau SDK pihak ketiga yang bentuknya ditentukan vendornya. Adapter Pattern menyambungkan kode-kode tersebut ke kontrak aplikasi tanpa mengubah salah satu sisi. Chapter ini membahas object adapter lewat composition, class adapter lewat inheritance, perbandingan keduanya, cara membungkus SDK, dan sinerginya dengan Open/Closed Principle.

Masalah: Kontrak Baru, Implementasi Lama #

Bayangkan aplikasi kita menetapkan kontrak pembayaran yang bersih — parameter bertipe dan hasil berupa ID transaksi — sementara di gudang masih tertinggal gerbang pembayaran lama yang serba array. Kedua sisi ini tidak cocok:

php
<?php

// Kode warisan: array asosiatif masuk, boolean keluar
final class GerbangPembayaranLama
{
    public function prosesPembayaran(array $data): bool
    {
        return ($data["amount"] ?? 0) > 0
            && str_starts_with((string) ($data["card"] ?? ""), "4");
    }
}

// Kontrak baru milik aplikasi: parameter bertipe, hasil berupa ID transaksi
interface PembayaranGateway
{
    public function charge(int $nominal, string $nomorKartu): string;
}

Method prosesPembayaran() memakai array asosiatif dan mengembalikan bool, sedangkan kontrak PembayaranGateway menuntut parameter bertipe serta string ID transaksi. Ada dua jalan keluar yang sama buruknya: mengubah class lama berisiko merusak pemakai lain yang belum sempat dimigrasi, dan menekuk kontrak baru agar meniru bentuk lama berarti mencemari seluruh aplikasi dengan gaya warisan. Adapter menawarkan jalan ketiga: satu class kecil yang mengenal kedua dunia dan menerjemahkan di antara keduanya.

Object Adapter lewat Composition #

Object adapter mengimplementasi kontrak baru sambil menyimpan instance lama sebagai field — pola composition yang sudah akrab bagi kita sejak pembahasan dependency injection:

php
<?php

final class GerbangPembayaranLama
{
    public function prosesPembayaran(array $data): bool
    {
        return ($data["amount"] ?? 0) > 0
            && str_starts_with((string) ($data["card"] ?? ""), "4");
    }
}

interface PembayaranGateway
{
    public function charge(int $nominal, string $nomorKartu): string;
}

final class AdapterGerbangLama implements PembayaranGateway
{
    public function __construct(private readonly GerbangPembayaranLama $gerbang)
    {
    }

    public function charge(int $nominal, string $nomorKartu): string
    {
        $berhasil = $this->gerbang->prosesPembayaran([
            "amount" => $nominal,
            "card" => $nomorKartu,
        ]);

        if (!$berhasil) {
            throw new RuntimeException("Gerbang lama menolak pembayaran");
        }

        return sprintf("GBR-%d-%s", $nominal, substr($nomorKartu, -4));
    }
}

$gateway = new AdapterGerbangLama(new GerbangPembayaranLama());

echo $gateway->charge(150000, "4242424242424242"), "\n";

try {
    $gateway->charge(150000, "5555000011112222");
} catch (RuntimeException $e) {
    echo "Ditolak: ", $e->getMessage(), "\n";
}

Output yang Dihasilkan:

text
GBR-150000-4242
Ditolak: Gerbang lama menolak pembayaran

Mari kita bedah penerjemahannya:

  1. Baris 17-21: AdapterGerbangLama mengimplementasi kontrak baru dan menyimpan instance lama di property readonly hasil constructor promotion — hanya empat baris untuk seluruh persiapan.
  2. Baris 23-35: charge() menerjemahkan dua arah. Ke dalam, parameter bertipe dirakit menjadi array asosiatif yang dimengerti gerbang lama; ke luar, hasil bool ditafsirkan — false melempar RuntimeException, true dikonversi menjadi ID transaksi berformat baru.
  3. Baris 38-46: pemanggil hanya melihat PembayaranGateway. Nama gerbang, bentuk array, dan gaya boolean tidak pernah bocor keluar adapter.

Perhatikan pula bahwa adapter menyeragamkan gaya error: kegagalan bool dari dunia lama diterjemahkan menjadi exception di dunia baru, sehingga seluruh aplikasi cukup satu pola penanganan kegagalan.

Class Adapter lewat Inheritance #

Bentuk kedua memakai warisan: adapter mewarisi class lama sekaligus mengimplementasi kontrak baru. Perhatikan bahwa class lama di bawah ini sengaja tidak dibuat final agar bisa diwarisi:

php
<?php

class GerbangPembayaranLama
{
    public function prosesPembayaran(array $data): bool
    {
        return ($data["amount"] ?? 0) > 0
            && str_starts_with((string) ($data["card"] ?? ""), "4");
    }
}

interface PembayaranGateway
{
    public function charge(int $nominal, string $nomorKartu): string;
}

final class AdapterKelasGerbang extends GerbangPembayaranLama implements PembayaranGateway
{
    public function charge(int $nominal, string $nomorKartu): string
    {
        $berhasil = $this->prosesPembayaran([
            "amount" => $nominal,
            "card" => $nomorKartu,
        ]);

        return $berhasil
            ? sprintf("GBR-%d-KELAS", $nominal)
            : "GAGAL-{$nominal}";
    }
}

$gateway = new AdapterKelasGerbang();
echo $gateway->charge(99000, "4000123412341234"), "\n";
echo $gateway->charge(99000, "5555000011112222"), "\n";

Output yang Dihasilkan:

text
GBR-99000-KELAS
GAGAL-99000

Baris 17 adalah inti polanya: extends GerbangPembayaranLama implements PembayaranGateway menyatukan kedua dunia dalam satu class, sehingga charge() cukup memanggil method warisan $this->prosesPembayaran() tanpa field tambahan. Namun baris terakhir contoh juga menyingkap kelemahannya: karena hanya mewarisi kontrak bool, class adapter ini terpaksa mengembalikan string penanda "GAGAL-99000" — gaya error campuran yang mudah lolos dari pemeriksaan pemanggil. Object adapter bebas dari masalah tersebut karena bekerja dari luar dan bebas menentukan kontrak kegagalan.

Class Adapter atau Object Adapter #

Kedua bentuk itu menyelesaikan masalah yang sama, tetapi batasannya sangat berbeda:

AspekClass adapterObject adapter
Mekanismeextends + implementscomposition, menyimpan instance lama
Syarat pada kode lamatidak final, boleh diwarisitanpa syarat — cukup memiliki objeknya
Slot extendsterpakai untuk class lamatetap bebas untuk kebutuhan lain
Beberapa layanan sekaligussulitmudah — bungkus beberapa objek sekaligus
Pengujianperilaku menyatu dengan warisansuntik instance lama yang palsu

PHP hanya mengizinkan satu jalur warisan per class, jadi slot extends itu mahal; sekali dipakai untuk class lama, slot tersebut tidak bisa dipakai untuk kebutuhan desain lain. SDK modern juga sering menandai class-nya final, yang menutup jalan class adapter sepenuhnya, sementara object adapter tetap bekerja karena hanya membutuhkan instance. Ditambah kemudahan menyuntik objek palsu saat pengujian, composition menjadi pilihan yang lebih cocok untuk mayoritas kasus PHP modern.

Membungkus SDK Pihak Ketiga #

Contoh paling sering dijumpai di produksi adalah SDK. Nama method berbahasa Inggris, payload JSON mentah, dan hasil bercampur kode status — kalau kode aplikasi memanggil SDK secara langsung, satu perubahan kecil dari vendor menjalar ke seluruh kode. Adapter menjadi lapisan penerjemah yang menjaga aplikasi tetap bicara dalam kontraknya sendiri:

php
<?php

// SDK pihak ketiga (ilustrasi): sepenuhnya di luar kendali kita
final class SdkKurirEksternal
{
    public function __construct(private readonly bool $kreditHabis = false)
    {
    }

    public function createShipment(string $payloadJson): string
    {
        if ($this->kreditHabis) {
            return '{"status":"error","message":"kredit habis"}';
        }

        return '{"status":"ok","waybill":"WB-7721"}';
    }
}

interface PengirimanApi
{
    public function buatPengiriman(string $alamat, int $beratGram): string;
}

final class AdapterSdkKurir implements PengirimanApi
{
    public function __construct(private readonly SdkKurirEksternal $sdk)
    {
    }

    public function buatPengiriman(string $alamat, int $beratGram): string
    {
        $payload = json_encode(["address" => $alamat, "weight" => $beratGram]);
        $respons = $this->sdk->createShipment($payload);

        $data = json_decode($respons, true);
        if (!is_array($data) || ($data["status"] ?? "") !== "ok") {
            throw new RuntimeException(
                "SDK kurir gagal: " . ($data["message"] ?? "respons tidak valid")
            );
        }

        return $data["waybill"];
    }
}

// Skenario normal
$api = new AdapterSdkKurir(new SdkKurirEksternal());
echo "Resi: ", $api->buatPengiriman("Jl. Melati 12, Bandung", 1500), "\n";

// Skenario uji: SDK dibuat selalu gagal, tanpa menyentuh jaringan
$apiGagal = new AdapterSdkKurir(new SdkKurirEksternal(kreditHabis: true));

try {
    $apiGagal->buatPengiriman("Jl. Melati 12, Bandung", 1500);
} catch (RuntimeException $e) {
    echo "Tertangkap: ", $e->getMessage(), "\n";
}

Output yang Dihasilkan:

text
Resi: WB-7721
Tertangkap: SDK kurir gagal: kredit habis

Alurnya berjalan tiga tahap. Pertama, baris 33 merakit payload json_encode dengan bentuk yang diminta SDK, sehingga pemanggil cukup mengirim $alamat dan $beratGram bertipe. Kedua, baris 36-41 mengurai respons json_decode dan memvalidasi statusnya; respons tidak valid atau bermuatan error berubah menjadi exception berisi pesan dari SDK. Ketiga, baris 47-58 memperlihatkan dua skenario sekaligus — baris 52 memakai named argument kreditHabis: true untuk membuat SDK yang selalu gagal, sehingga jalur error adapter teruji tanpa pernah menyentuh jaringan sungguhan.

Sinergi dengan Open/Closed Principle #

Kekuatan adapter baru terasa utuh saat digabungkan dengan layanan yang hanya mengenal kontrak:

php
<?php

final class LayananCheckout
{
    public function __construct(private readonly PembayaranGateway $gateway)
    {
    }

    public function bayar(int $nominal, string $nomorKartu): string
    {
        // ... validasi keranjang, membuat pesanan ...

        return $this->gateway->charge($nominal, $nomorKartu);
    }
}

LayananCheckout sama sekali tidak tahu gerbang mana yang bekerja di belakangnya. Menambah gerbang baru — misalnya penyedia modern berbasis API — hanya berarti menulis adapter baru plus satu baris penyambungan di composition root; tidak ada satu baris pun di layanan yang berubah. Itulah definisi harfiah terbuka-untuk-perluasan, tertutup-untuk-modifikasi yang kita dalami di chapter Open/Closed Principle , dan pemetaan kode ke gerbang yang tepat bisa diserahkan kepada factory dari chapter Factory Pattern .

Ringkasan #

Adapter Pattern menyambungkan dua antarmuka yang tidak cocok: object adapter melakukannya lewat composition dan menjadi pilihan baku PHP modern, class adapter lewat warisan dengan syarat class lama bisa diwarisi, dan keduanya sama-sama sanggup membungkus SDK atau kode warisan agar aplikasi tetap bicara dalam kontraknya sendiri. Digabungkan dengan layanan yang hanya bergantung pada kontrak, adapter membuat penyedia baru masuk tanpa menyentuh kode lama. Pattern berikutnya menghadapi kasus yang sebaliknya — antarmuka sudah cocok, tetapi perilakunya ingin diperkaya: Decorator Pattern membungkus objek dengan objek sejenis untuk menambah lapisan perilaku tanpa ledakan subclass.

Referensi #