Kode baru dan kode lama jarang bicara bahasa yang sama. Kita ingin seluruh layanan memakai interface Logger dengan method Log(level, msg), sementara library logging lama — milik pihak ketiga — hanya menyediakan Print(v ...any) tanpa konsep level. Mengubah library itu mustahil karena sumbernya bukan milik kita, dan menulis ulang seluruh layanan demi satu API lama juga terlalu mahal. Adapter pattern menyelesaikan kebuntuan ini dengan satu struct kecil yang menerjemahkan antarmuka baru menjadi panggilan lama, tanpa menyentuh salah satu sisi.

Tiga Peran: Target, Adaptee, dan Adapter #

Adapter selalu melibatkan tiga peran. Target adalah interface yang diharapkan kode baru — dalam chapter ini, Logger dengan method Log. Adaptee adalah implementasi lama dengan API yang berbeda — LoggerLama dengan method Print. Adapter adalah struct yang mengimplementasi target dengan cara mendelegasikan setiap panggilan ke adaptee, sambil menerjemahkan argumen di antara keduanya.

Perhatikan arah panahnya: Layanan hanya bergantung pada Logger, dan AdapterLogger adalah satu-satunya pihak yang tahu LoggerLama ada. Idiom Go untuk pola ini sederhana — interface kecil sebagai target, struct wrapper sebagai adapter, dan method adapter yang memanggil adaptee di dalamnya. Bila sebagian method adaptee sudah cocok, adaptee bisa di-embed ke dalam adapter agar method tersebut terwarisi secara otomatis; method yang bentuknya berbeda tetap ditulis manual.

Satu keistimewaan Go membuat pola ini terasa ringan: interface dipenuhi secara implisit. AdapterLogger tidak pernah mendeklarasikan bahwa struct tersebut mengimplementasi Logger — kecocokan method diperiksa kompiler begitu nilai tersebut dipakai sebagai Logger. Akibatnya, adapter dapat dibuat untuk tipe pihak ketiga yang tidak bisa kita sentuh sama sekali, sesuatu yang di bahasa lain menuntut deklarasi implements pada definisi class.

Adapter juga tidak harus struct. Standard library memakai bentuk fungsi pada http.HandlerFunc: fungsi pembungkus mengubah func(w http.ResponseWriter, r *http.Request) menjadi tipe yang memenuhi interface http.Handler. Prinsipnya sama dengan AdapterLogger — hanya mediumnya yang berbeda — dan pola pembungkusan fungsi ini akan muncul kembali pada chapter Middleware Pattern .

Demo: LoggerLama Menjadi Logger #

Berkas adapter-demo.go pada bagian bawah halaman membangun ketiga peran tersebut secara utuh. Layanan baru memanggil Log("info", ...) dan Log("warn", ...), sementara keluaran tetap dihasilkan oleh Print milik LoggerLama. Kode lengkapnya adalah sebagai berikut:

go
package main

import (
	"fmt"
	"strings"
)

// Logger adalah target interface yang diharapkan seluruh kode baru.
// Hanya dua method kecil: level dan pesan.
type Logger interface {
	Log(level, msg string)
}

// LoggerLama adalah API lama milik pihak ketiga: method Print
// menerima argumen apa tanpa konsep level. Kita tidak bisa
// mengubahnya karena kode sumbernya bukan milik kita.
type LoggerLama struct {
	Awalan string
}

// Print milik LoggerLama menggabungkan seluruh argumen lalu mencetak.
func (l *LoggerLama) Print(v ...any) {
	fmt.Println(l.Awalan, fmt.Sprint(v...))
}

// AdapterLogger adalah adapter: mengimplementasi Logger
// dengan mendelegasikan setiap panggilan ke LoggerLama.
type AdapterLogger struct {
	lama *LoggerLama
}

// Log menerjemahkan kontrak baru menjadi kontrak lama:
// level dan pesan digabung menjadi satu argumen Print.
func (a *AdapterLogger) Log(level, msg string) {
	a.lama.Print("[", strings.ToUpper(level), "] ", msg)
}

// Verifikasi saat kompilasi: AdapterLogger wajib memenuhi Logger.
var _ Logger = (*AdapterLogger)(nil)

// Layanan adalah client yang hanya mengenal target interface.
// Layanan tidak tahu menahu soal LoggerLama.
type Layanan struct {
	log Logger
}

func (s *Layanan) Proses(namaTugas string, detik int) {
	s.log.Log("info", "mulai tugas "+namaTugas)
	if detik > 3 {
		s.log.Log("warn", "tugas "+namaTugas+" melebihi batas waktu")
	}
	s.log.Log("info", "tugas "+namaTugas+" selesai")
}

func main() {
	lama := &LoggerLama{Awalan: "app:"}
	svc := &Layanan{log: &AdapterLogger{lama: lama}}
	svc.Proses("sinkronisasi", 5)
}

Output yang Dihasilkan:

text
app: [INFO] mulai tugas sinkronisasi
app: [WARN] tugas sinkronisasi melebihi batas waktu
app: [INFO] tugas sinkronisasi selesai

Mari kita telaah baris demi baris program di atas:

  1. Baris 10–12 (type Logger interface): target interface — satu method kecil dengan level dan pesan; ini satu-satunya kontrak yang dikenal Layanan.
  2. Baris 17–24 (LoggerLama): adaptee pihak ketiga; method Print menerima argumen apa pun dan mencetaknya dengan awalan, tanpa konsep level.
  3. Baris 28–30 (AdapterLogger): adapter menyimpan adaptee sebagai field; tidak ada logika lain yang dibawa.
  4. Baris 34–36 (method Log): inti penerjemahan — level dihurufbesarkan, dibungkus tanda kurung siku, lalu seluruh potongan dikirim ke Print sebagai argumen terpisah.
  5. Baris 39 (var _ Logger = ...): verifikasi saat kompilasi bahwa adapter benar-benar memenuhi target; baris ini gagal dikompilasi bila method Log berubah bentuk.
  6. Baris 47–53 (Proses): client memakai Log biasa; baris ini tidak akan berubah apa pun yang terjadi pada LoggerLama.
  7. Baris 55–59 (main): perakitan hanya terjadi sekali — adaptee dibuat, dibungkus adapter, lalu disuntikkan ke Layanan.

Kenapa Tidak Mengubah LoggerLama Langsung #

Pertanyaan ini hampir selalu muncul, dan jawabannya berlapis. Lapis pertama: kepemilikan. Kode pihak ketiga di luar kendali kita; mengubahnya berarti memelihara fork sendiri. Lapis kedua: pemanggil lain. LoggerLama biasanya masih dipakai banyak kode lama yang tidak siap dimigrasi; mengubah signature Print akan merusak semuanya sekaligus. Lapis ketiga: transisi bertahap. Adapter memungkinkan kode baru berpindah ke interface Logger hari ini, sementara kode lama menyusul — keduanya hidup berdampingan sampai adaptee benar-benar pensiun.

Adapter menyatukan dua antarmuka yang sudah ada. Chapter berikutnya menghadapi variasi yang hidup berdampingan di dalam satu program: Strategy Pattern memungkinkan algoritma diganti saat program berjalan tanpa mengubah kode pemanggilnya.

Referensi #

Berkas Sumber

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

adapter-demo.go

Unduh berkas
go
package main

import (
	"fmt"
	"strings"
)

// Logger adalah target interface yang diharapkan seluruh kode baru.
// Hanya dua method kecil: level dan pesan.
type Logger interface {
	Log(level, msg string)
}

// LoggerLama adalah API lama milik pihak ketiga: method Print
// menerima argumen apa tanpa konsep level. Kita tidak bisa
// mengubahnya karena kode sumbernya bukan milik kita.
type LoggerLama struct {
	Awalan string
}

// Print milik LoggerLama menggabungkan seluruh argumen lalu mencetak.
func (l *LoggerLama) Print(v ...any) {
	fmt.Println(l.Awalan, fmt.Sprint(v...))
}

// AdapterLogger adalah adapter: mengimplementasi Logger
// dengan mendelegasikan setiap panggilan ke LoggerLama.
type AdapterLogger struct {
	lama *LoggerLama
}

// Log menerjemahkan kontrak baru menjadi kontrak lama:
// level dan pesan digabung menjadi satu argumen Print.
func (a *AdapterLogger) Log(level, msg string) {
	a.lama.Print("[", strings.ToUpper(level), "] ", msg)
}

// Verifikasi saat kompilasi: AdapterLogger wajib memenuhi Logger.
var _ Logger = (*AdapterLogger)(nil)

// Layanan adalah client yang hanya mengenal target interface.
// Layanan tidak tahu menahu soal LoggerLama.
type Layanan struct {
	log Logger
}

func (s *Layanan) Proses(namaTugas string, detik int) {
	s.log.Log("info", "mulai tugas "+namaTugas)
	if detik > 3 {
		s.log.Log("warn", "tugas "+namaTugas+" melebihi batas waktu")
	}
	s.log.Log("info", "tugas "+namaTugas+" selesai")
}

func main() {
	lama := &LoggerLama{Awalan: "app:"}
	svc := &Layanan{log: &AdapterLogger{lama: lama}}
	svc.Proses("sinkronisasi", 5)
}