Apa itu Adapter Pattern? Penjelasan Lengkap untuk Pemula
Pendahuluan
Pernahkah kamu mencoba mencolokkan charger laptop ke stopkontak yang bentuknya berbeda? Di beberapa negara, bentuk colokan listrik berbeda-beda. Solusinya? Kamu menggunakan adapter — alat kecil yang menjembatani dua antarmuka yang tidak kompatibel.
Di dunia pemrograman, masalah yang sama sering terjadi. Kamu punya dua komponen yang ingin bekerja sama, tapi antarmukanya tidak cocok. Di sinilah Adapter Pattern hadir sebagai solusi elegan.
Adapter Pattern adalah salah satu structural design pattern yang memungkinkan dua antarmuka yang tidak kompatibel untuk bekerja bersama tanpa mengubah kode yang sudah ada. Bagi kamu yang baru mulai belajar design pattern, artikel ini akan memandu kamu dari dasar dengan penjelasan sederhana dan contoh kode yang bisa langsung dijalankan.
Memahami Konsep Adapter Pattern
Analoginya di Dunia Nyata
Bayangkan kamu sedang membangun aplikasi pembayaran seperti layanan e-wallet. Di versi lama, aplikasimu menggunakan sistem pembayaran lokal dengan metode bayar(jumlah). Tapi kini kamu ingin mengintegrasikan gateway pembayaran internasional yang menggunakan metode processPayment(amount, currency).
Masalahnya: kode lama tidak bisa diganti begitu saja karena sudah digunakan di ratusan tempat. Di sinilah Adapter Pattern bekerja — ia membuat “penerjemah” di antara keduanya.
Komponen Utama Adapter Pattern
Adapter Pattern terdiri dari empat elemen:
- Target Interface — antarmuka yang diharapkan oleh kode klien
- Adaptee — kelas yang sudah ada tapi antarmukanya tidak kompatibel
- Adapter — kelas penghubung yang menerjemahkan panggilan dari Target ke Adaptee
- Client — kode yang menggunakan Target Interface
Client → Target Interface ← Adapter → Adaptee
Implementasi Adapter Pattern dengan Contoh Kode
Mari kita lihat implementasi konkret menggunakan Python dan TypeScript.
Contoh 1: Python — Integrasi Sistem Pembayaran
# Adaptee: sistem pembayaran lama yang sudah ada dan tidak bisa diubah
class SistemPembayaranLama:
def bayar(self, jumlah: int) -> str:
# Method lama hanya menerima jumlah dalam rupiah
return f"Pembayaran Rp{jumlah:,} berhasil diproses"
# Target Interface: bentuk method yang diharapkan oleh sistem baru
class PembayaranInterface:
def process_payment(self, amount: float, currency: str) -> str:
raise NotImplementedError("Method ini harus diimplementasikan")
# Adapter: menghubungkan sistem lama dengan interface baru
class PembayaranAdapter(PembayaranInterface):
def __init__(self, sistem_lama: SistemPembayaranLama):
# Menyimpan objek dari sistem lama
self.sistem_lama = sistem_lama
def process_payment(self, amount: float, currency: str) -> str:
# Jika mata uang USD, ubah dulu ke rupiah
if currency == "USD":
amount_idr = int(amount * 16000)
else:
# Jika sudah IDR, langsung ubah ke integer
amount_idr = int(amount)
# Adapter memanggil method lama dengan data yang sudah disesuaikan
return self.sistem_lama.bayar(amount_idr)
# Client: kode baru hanya mengenal interface pembayaran
def proses_checkout(pembayaran: PembayaranInterface, amount: float, currency: str) -> None:
hasil = pembayaran.process_payment(amount, currency)
print(f"Checkout selesai: {hasil}")
# Program utama
if __name__ == "__main__":
# Membuat objek sistem lama
sistem_lama = SistemPembayaranLama()
# Membungkus sistem lama dengan adapter
adapter = PembayaranAdapter(sistem_lama)
# Client memakai adapter seolah-olah itu sistem baru
proses_checkout(adapter, 50.0, "USD")
proses_checkout(adapter, 150000, "IDR")
# Output:
# Checkout selesai: Pembayaran Rp800,000 berhasil diproses
# Checkout selesai: Pembayaran Rp150,000 berhasil diproses
Contoh 2: TypeScript — Adapter untuk Library Logging
Jika kamu sudah familiar dengan Pengenalan TypeScript: Superset JavaScript yang Lebih Aman, kamu tahu betapa pentingnya type-safety. Berikut contoh Adapter Pattern dalam TypeScript:
// Target interface yang akan dipakai oleh kode kita
interface Logger {
log(message: string): void;
warn(message: string): void;
error(message: string): void;
}
// Adaptee: logger pihak ketiga dengan format method yang berbeda
class ThirdPartyLogger {
writeLog(level: string, message: string, timestamp: Date): void {
console.log(`[${timestamp.toISOString()}] [${level.toUpperCase()}] ${message}`);
}
}
// Adapter: mengubah API ThirdPartyLogger agar cocok dengan interface Logger
class LoggerAdapter implements Logger {
constructor(private thirdPartyLogger: ThirdPartyLogger) {}
log(message: string): void {
this.thirdPartyLogger.writeLog("info", message, new Date());
}
warn(message: string): void {
this.thirdPartyLogger.writeLog("warn", message, new Date());
}
error(message: string): void {
this.thirdPartyLogger.writeLog("error", message, new Date());
}
}
// Client hanya mengenal interface Logger, bukan class logger tertentu
function prosesData(logger: Logger, data: string[]): void {
logger.log("Mulai memproses data...");
if (data.length === 0) {
logger.warn("Data kosong, tidak ada yang diproses");
return;
}
for (const item of data) {
logger.log(`Memproses: ${item}`);
}
logger.log("Selesai!");
}
// Membuat object logger pihak ketiga
const thirdPartyLogger = new ThirdPartyLogger();
// Membungkus logger pihak ketiga dengan adapter
const logger: Logger = new LoggerAdapter(thirdPartyLogger);
// Menjalankan program
prosesData(logger, ["user-1", "user-2", "user-3"]);
// Output:
// [2026-04-06T...Z] [INFO] Mulai memproses data...
// [2026-04-06T...Z] [INFO] Memproses: user-1
// [2026-04-06T...Z] [INFO] Memproses: user-2
// [2026-04-06T...Z] [INFO] Memproses: user-3
// [2026-04-06T...Z] [INFO] Selesai!
Kapan dan Mengapa Menggunakan Adapter Pattern
Gunakan Adapter Pattern ketika:
- Mengintegrasikan library pihak ketiga yang tidak bisa dimodifikasi
- Mempertahankan kode lama (legacy) sambil menambahkan fitur baru
- Menyatukan berbagai API yang punya perilaku serupa tapi antarmuka berbeda
- Testing — membuat adapter ke mock/stub untuk unit test
Jika kamu sedang belajar tentang pengelolaan kode yang terstruktur, konsep ini sangat berkaitan dengan topik yang dibahas di Mastering ES6 Modules: Cara Mengelola Kode JavaScript Secara Terstruktur — keduanya bertujuan menjaga kode tetap modular dan mudah dikelola.
Keuntungan Adapter Pattern
| Keuntungan | Penjelasan |
|---|---|
| Open/Closed Principle | Kode lama tidak perlu diubah |
| Single Responsibility | Logika konversi terpusat di satu tempat |
| Reusability | Adapter bisa digunakan ulang di banyak tempat |
| Testability | Mudah di-mock untuk unit testing |
Contoh Kasus Nyata
Kasus 1: Migrasi Database
Bayangkan kamu ingin membangun sistem manajemen data seperti yang digunakan platform e-commerce besar. Kamu memiliki kode yang bergantung pada MySQL, tapi ingin migrasi ke PostgreSQL:
# Adaptee: class baru untuk koneksi PostgreSQL
class PostgreSQLConnection:
def execute_query(self, sql: str, params: tuple) -> list:
# Simulasi menjalankan query ke PostgreSQL
print(f"[PostgreSQL] Menjalankan: {sql}")
print(f"[PostgreSQL] Parameter: {params}")
# Simulasi hasil data dari database
return [{"id": params[0], "nama": "Budi"}]
# Target Interface: bentuk method yang diharapkan kode lama
class DatabaseInterface:
def query(self, sql: str, *args) -> list:
raise NotImplementedError("Method query() harus diimplementasikan")
# Adapter: menjembatani interface lama ke class PostgreSQL baru
class PostgreSQLAdapter(DatabaseInterface):
def __init__(self, pg_conn: PostgreSQLConnection):
# Simpan object koneksi PostgreSQL
self.pg_conn = pg_conn
def query(self, sql: str, *args) -> list:
# Ubah *args menjadi tuple agar cocok dengan execute_query()
return self.pg_conn.execute_query(sql, args)
# Kode lama tetap memakai interface DatabaseInterface
def ambil_pengguna(db: DatabaseInterface, user_id: int):
# Kode lama memanggil method query() seperti biasa
return db.query("SELECT * FROM users WHERE id = %s", user_id)
# Program utama
pg = PostgreSQLConnection() # Buat koneksi PostgreSQL
adapter = PostgreSQLAdapter(pg) # Bungkus dengan adapter
hasil = ambil_pengguna(adapter, 42) # Panggil fungsi lama tanpa perubahan
# Tampilkan hasil query
print("Hasil:", hasil)
# Output:
# [PostgreSQL] Menjalankan: SELECT * FROM users WHERE id = %s
# [PostgreSQL] Parameter: (42,)
# Hasil: [{'id': 42, 'nama': 'Budi'}]
Troubleshooting: Error yang Sering Muncul
Adapter Tidak Mengimplementasikan Semua Method Interface
Penyebab: Ketika Target Interface memiliki banyak method, developer sering lupa mengimplementasikan salah satunya di Adapter, menyebabkan error TypeError atau NotImplementedError saat runtime.
Solusi: Gunakan Abstract Base Class (ABC) agar Python langsung memberikan error saat class dibuat, bukan saat method dipanggil.
from abc import ABC, abstractmethod
# ABC membuat "kontrak" yang wajib dipenuhi oleh class turunan
class PembayaranInterface(ABC):
@abstractmethod
def process_payment(self, amount: float, currency: str) -> str:
pass # Wajib diimplementasikan di class turunan
@abstractmethod
def refund(self, transaction_id: str) -> bool:
pass # Wajib diimplementasikan di class turunan
# Adapter wajib mengimplementasikan semua method abstract
class PembayaranAdapter(PembayaranInterface):
def process_payment(self, amount: float, currency: str) -> str:
return f"Pembayaran {amount} {currency} berhasil"
def refund(self, transaction_id: str) -> bool:
return True
# Membuat objek dan menjalankan method
pembayaran = PembayaranAdapter()
hasil_pembayaran = pembayaran.process_payment(100000, "IDR")
print(hasil_pembayaran)
hasil_refund = pembayaran.refund("TRX001")
print(f"Refund berhasil: {hasil_refund}")
# Output:
# Pembayaran 100000 IDR berhasil
# Refund berhasil: True
Circular Dependency Antara Adapter dan Adaptee
Penyebab: Adapter mengimpor Adaptee, dan Adaptee juga mengimpor Adapter (langsung atau tidak langsung), menyebabkan ImportError atau CircularImportError.
Solusi: Gunakan dependency injection — kirim objek Adaptee dari luar, jangan impor langsung di dalam Adapter.
# adaptee.py — tidak mengimpor apa pun dari adapter
class SistemLama:
def proses_pembayaran(self, jumlah):
return f"Pembayaran sebesar Rp{jumlah:,} berhasil diproses oleh sistem lama"
# adapter.py — menerima objek SistemLama dari luar, bukan mengimpornya
class PembayaranAdapter:
def __init__(self, sistem_lama):
# Dependency injection: objek dikirim dari luar
self.sistem_lama = sistem_lama
def bayar(self, jumlah):
return self.sistem_lama.proses_pembayaran(jumlah)
# main.py — bertanggung jawab membuat dan menghubungkan semua objek
sistem_lama = SistemLama()
adapter = PembayaranAdapter(sistem_lama)
hasil = adapter.bayar(50000)
print(hasil)
# Output:
# Pembayaran sebesar Rp50,000 berhasil diproses oleh sistem lama
TypeScript: Type Mismatch pada Return Type Adapter
Penyebab: Method di Adapter mengembalikan tipe yang tidak sesuai dengan yang didefinisikan di interface, menyebabkan error kompilasi TypeScript.
Solusi: Pastikan signature method di Adapter persis sama dengan yang dideklarasikan di interface.
// Interface mendefinisikan kontrak: log harus mengembalikan void
interface Logger {
log(message: string): void;
}
// SALAH — return type string tidak cocok dengan void di interface
// class LoggerAdapter implements Logger {
// log(message: string): string { // Error: Type 'string' is not assignable to type 'void'
// return "logged";
// }
// }
// BENAR — return type void sesuai dengan yang dideklarasikan interface
class LoggerAdapter implements Logger {
log(message: string): void {
console.log(`[LOG] ${message}`);
}
}
// Membuat object dan memanggil method
const logger = new LoggerAdapter();
logger.log("Halo, dunia!");
logger.log("Belajar structural typing TypeScript");
// Output:
// [LOG] Halo, dunia!
// [LOG] Belajar structural typing TypeScript
Pertanyaan yang Sering Diajukan
Apa perbedaan Adapter Pattern dengan Decorator Pattern?
Adapter Pattern mengubah antarmuka suatu objek agar kompatibel dengan yang lain, sedangkan Decorator Pattern menambahkan fungsionalitas baru tanpa mengubah antarmuka. Sederhananya: Adapter adalah penerjemah antarmuka, sedangkan Decorator adalah penambah kemampuan pada antarmuka yang sudah ada.
Apakah Adapter Pattern memperlambat performa aplikasi?
Secara umum tidak signifikan. Adapter hanya menambahkan satu lapisan pemanggilan fungsi yang sangat tipis. Overhead yang ditimbulkan sangat kecil dibandingkan manfaat maintainability-nya, dan biasanya tidak terasa sama sekali di aplikasi dunia nyata.
Kapan sebaiknya TIDAK menggunakan Adapter Pattern?
Jika kamu sedang membangun sistem baru dari nol dan bisa mendesain antarmuka sendiri, lebih baik langsung buat antarmuka yang konsisten dari awal. Adapter paling berguna saat berurusan dengan kode lama atau library pihak ketiga yang tidak bisa dimodifikasi.
Bagaimana cara membedakan Adapter Pattern dengan Bridge Pattern?
Adapter Pattern digunakan setelah ada masalah kompatibilitas (solusi retroaktif), sedangkan Bridge Pattern didesain sejak awal untuk memisahkan abstraksi dari implementasinya. Adapter adalah “perbaikan darurat yang elegan”, Bridge adalah “desain yang terencana sejak awal”.
Apakah Adapter Pattern bisa digunakan untuk testing?
Ya, ini salah satu penggunaan terpopuler! Kamu bisa membuat TestAdapter yang mengimplementasikan interface yang sama tapi mengembalikan data palsu (mock), sehingga unit test tidak perlu menyentuh database atau API eksternal sungguhan.
Kesimpulan
Adapter Pattern adalah solusi yang elegan untuk masalah yang sangat umum dalam pengembangan perangkat lunak: dua komponen yang ingin bekerja sama tapi tidak bisa karena antarmuka yang berbeda. Dengan membuat lapisan penerjemah di antaranya, kamu bisa mengintegrasikan kode lama, library pihak ketiga, atau sistem eksternal tanpa mengubah satu baris pun dari kode yang sudah berjalan.
Kunci utamanya: identifikasi Target Interface yang kamu inginkan, kenali Adaptee yang sudah ada, lalu buat Adapter yang menjembatani keduanya. Pola ini menjaga kode tetap bersih, modular, dan mudah diuji.
Selamat belajar dan terus berlatih! Design pattern seperti Adapter adalah investasi jangka panjang yang akan sangat membantu saat proyekmu berkembang — jangan ragu untuk eksplorasi artikel-artikel lainnya di KamusNgoding dan terus tingkatkan kemampuanmu sebagai developer Indonesia yang handal.