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
// 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
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:
GBR-150000-4242
Ditolak: Gerbang lama menolak pembayaranMari kita bedah penerjemahannya:
- Baris 17-21:
AdapterGerbangLamamengimplementasi kontrak baru dan menyimpan instance lama di propertyreadonlyhasil constructor promotion — hanya empat baris untuk seluruh persiapan. - Baris 23-35:
charge()menerjemahkan dua arah. Ke dalam, parameter bertipe dirakit menjadi array asosiatif yang dimengerti gerbang lama; ke luar, hasilboolditafsirkan —falsemelemparRuntimeException,truedikonversi menjadi ID transaksi berformat baru. - 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
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:
GBR-99000-KELAS
GAGAL-99000Baris 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:
| Aspek | Class adapter | Object adapter |
|---|---|---|
| Mekanisme | extends + implements | composition, menyimpan instance lama |
| Syarat pada kode lama | tidak final, boleh diwarisi | tanpa syarat — cukup memiliki objeknya |
Slot extends | terpakai untuk class lama | tetap bebas untuk kebutuhan lain |
| Beberapa layanan sekaligus | sulit | mudah — bungkus beberapa objek sekaligus |
| Pengujian | perilaku menyatu dengan warisan | suntik 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
// 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:
Resi: WB-7721
Tertangkap: SDK kurir gagal: kredit habisAlurnya 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
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.