Adapter Pattern #
Kamu baru saja menemukan library Go yang sempurna untuk kebutuhan SMS gateway — fiturnya lengkap, performanya bagus, dan sudah battle-tested. Satu masalah: library itu mengekspos method SendTextMessage(phone, body string) (string, error), sementara seluruh sistem kamu sudah bergantung pada interface Notifier dengan method Notify(recipient, message string) error. Kamu tidak bisa mengubah library pihak ketiga, dan mengubah interface Notifier berarti memodifikasi puluhan file yang sudah berjalan di production. Adapter Pattern hadir untuk situasi persis seperti ini — ia membuat dua interface yang tidak kompatibel bisa bekerja bersama tanpa mengubah satu pun dari keduanya, cukup dengan menambahkan lapisan tipis penerjemah di antara mereka.
Apa itu Adapter Pattern? #
Adapter Pattern adalah structural design pattern yang mengonversi interface sebuah objek menjadi interface lain yang diharapkan client. Seperti adaptor colokan listrik yang memungkinkan perangkat bercolokan tipe A digunakan di stopkontak tipe C, Adapter Pattern memungkinkan dua komponen yang “berbicara bahasa berbeda” untuk berkolaborasi tanpa salah satu harus berubah.
Ada tiga pihak dalam skenario Adapter:
- Client — kode yang sudah ada dan mengharapkan interface tertentu (Target)
- Adaptee — objek yang sudah ada dengan interface berbeda yang ingin digunakan
- Adapter — lapisan tipis yang mengimplementasikan interface Target, mendelegasikan panggilan ke Adaptee
Adapter Pattern adalah jawaban untuk pertanyaan “bagaimana saya menggunakan X tanpa mengubah Y?” — dan ini adalah pertanyaan yang sangat sering muncul saat mengintegrasikan library pihak ketiga, memigrasikan sistem lama, atau menghubungkan antar microservice dengan kontrak yang berbeda.
flowchart LR
subgraph "Tanpa Adapter — Tidak Bisa"
C1[Client\nbutuh Notify] -. "interface\ntidak cocok" .-> A1[SMS Library\npunya SendTextMessage]
end
subgraph "Dengan Adapter — Bisa"
C2[Client\nbutuh Notify] -->|Notify| AD[SMSAdapter\nimplementasi Notify]
AD -->|SendTextMessage| A2[SMS Library]
end
Anatomi Masalah yang Dipecahkan #
Sebelum melihat implementasi, penting memahami tiga skenario nyata di mana Adapter menjadi solusi yang tepat — bukan hanya “interface berbeda” secara abstrak.
Skenario 1: Integrasi Third-Party Library #
Library pihak ketiga tidak bisa kamu ubah, tapi interfacenya tidak cocok dengan sistem kamu. Ini adalah use case paling umum Adapter.
// ANTI-PATTERN: client langsung bergantung pada library konkret
import "github.com/twilio/twilio-go"
type NotificationService struct {
twilioClient *twilio.RestClient // bergantung pada implementasi konkret
}
func (s *NotificationService) SendAlert(phone, message string) error {
// Langsung memanggil API Twilio
params := &openapi.CreateMessageParams{}
params.SetTo(phone)
params.SetBody(message)
_, err := s.twilioClient.Api.CreateMessage(params)
return err
}
// Masalah: mengganti Twilio dengan provider lain = ubah semua kode ini
// BENAR: client bergantung pada interface — adapter menangani detail library
type SMSProvider interface {
SendSMS(phone, message string) error
}
type NotificationService struct {
sms SMSProvider // interface, bukan concrete type
}
func (s *NotificationService) SendAlert(phone, message string) error {
return s.sms.SendSMS(phone, message)
}
// Mengganti Twilio dengan Vonage = ganti adapter saja, NotificationService tidak berubah
Skenario 2: Migrasi Sistem Legacy #
Saat migrasi sistem bertahap, kode lama dan kode baru harus bisa hidup berdampingan. Adapter memungkinkan kode baru menggunakan interface modern sementara implementasinya masih delegasi ke sistem lama.
Skenario 3: Menyatukan Beberapa Provider #
Ketika ada beberapa vendor dengan API berbeda-beda yang harus tampil seragam di mata kode bisnis — payment gateway Midtrans, Xendit, dan Stripe misalnya — adapter menyeragamkan ketiganya di balik satu interface yang konsisten.
Struktur dan Komponen #
Adapter Pattern melibatkan empat komponen dengan peran yang jelas dan tidak tumpang tindih.
flowchart TD
subgraph Client Layer
C[OrderService\nclient code]
end
subgraph Target Interface
T[PaymentProcessor\ninterface\n+Pay amount error]
end
subgraph Adapter Layer
MA[MidtransAdapter\n+Pay amount error]
XA[XenditAdapter\n+Pay amount error]
end
subgraph Adaptee Layer
M[Midtrans SDK\n+ChargeWithCreditCard]
X[Xendit SDK\n+CreateInvoice]
end
C -->|hanya tahu| T
T --> MA
T --> XA
MA -->|translate| M
XA -->|translate| X
| Komponen | Peran | Yang Diubah Saat Ganti Provider |
|---|---|---|
| Target (interface) | Kontrak yang diharapkan client | Tidak pernah berubah |
| Client | Menggunakan Target interface | Tidak pernah berubah |
| Adapter | Menerjemahkan Target ke Adaptee | Ganti atau tambah Adapter baru |
| Adaptee | Library/sistem dengan interface berbeda | Tidak disentuh |
Implementasi Lengkap: Multi Payment Gateway #
Mari bangun sistem payment yang bisa bekerja dengan Midtrans, Xendit, dan Stripe — ketiganya memiliki API yang sangat berbeda — di balik satu interface yang seragam.
Target Interface #
Interface ini adalah “bahasa” yang dipahami oleh seluruh kode bisnis. Semua adapter harus mengimplementasikan interface ini.
package payment
import "time"
// TransactionResult menyimpan hasil transaksi yang berhasil.
type TransactionResult struct {
TransactionID string
Amount int
Currency string
Method string
ProcessedAt time.Time
RedirectURL string // untuk payment yang butuh redirect (ewallet, VA)
}
// PaymentProcessor adalah Target interface — kontrak untuk semua payment provider.
// Kode bisnis hanya mengenal interface ini; tidak ada satu pun provider konkret yang disebutkan.
type PaymentProcessor interface {
// Pay memproses pembayaran dan mengembalikan hasil transaksi.
Pay(amount int, currency, description string) (*TransactionResult, error)
// Refund membatalkan transaksi dan mengembalikan dana.
Refund(transactionID string, amount int) error
// CheckStatus memeriksa status transaksi berdasarkan ID-nya.
CheckStatus(transactionID string) (string, error)
// ProviderName mengembalikan nama provider untuk logging dan audit.
ProviderName() string
}
Adaptee 1: Midtrans SDK #
Midtrans menggunakan konsep “charge” dengan struktur request yang spesifik untuk setiap metode pembayaran.
package midtrans
// Ini merepresentasikan Midtrans Go SDK yang tidak bisa kamu modifikasi.
// Interface-nya sama sekali berbeda dari PaymentProcessor.
type ChargeRequest struct {
PaymentType string
TransactionID string
GrossAmount int64
Description string
CustomerName string
CustomerEmail string
}
type ChargeResponse struct {
TransactionID string
OrderID string
GrossAmount string
PaymentType string
TransactionStatus string
FraudStatus string
RedirectURL string
}
type Client struct {
serverKey string
baseURL string
}
func NewClient(serverKey, baseURL string) *Client {
return &Client{serverKey: serverKey, baseURL: baseURL}
}
func (c *Client) Charge(req ChargeRequest) (*ChargeResponse, error) {
// Implementasi Midtrans API call
return &ChargeResponse{
TransactionID: "midtrans-" + req.TransactionID,
TransactionStatus: "capture",
}, nil
}
func (c *Client) CancelTransaction(transactionID string) error {
// Implementasi cancel via Midtrans API
return nil
}
func (c *Client) GetTransactionStatus(transactionID string) (*ChargeResponse, error) {
return &ChargeResponse{TransactionStatus: "capture"}, nil
}
Adaptee 2: Xendit SDK #
Xendit menggunakan konsep “invoice” yang berbeda jauh dari Midtrans.
package xendit
// Xendit Go SDK — interface berbeda lagi.
type InvoiceRequest struct {
ExternalID string
Amount float64
Description string
PayerEmail string
Currency string
CallbackURL string
SuccessURL string
}
type InvoiceResponse struct {
ID string
ExternalID string
Amount float64
Status string
InvoiceURL string
Created string
Expiry string
}
type Client struct {
apiKey string
}
func NewClient(apiKey string) *Client {
return &Client{apiKey: apiKey}
}
func (c *Client) CreateInvoice(req InvoiceRequest) (*InvoiceResponse, error) {
return &InvoiceResponse{
ID: "xendit-inv-123",
Status: "PENDING",
InvoiceURL: "https://checkout.xendit.co/inv/xendit-inv-123",
}, nil
}
func (c *Client) ExpireInvoice(invoiceID string) error {
return nil
}
func (c *Client) GetInvoice(invoiceID string) (*InvoiceResponse, error) {
return &InvoiceResponse{Status: "PAID"}, nil
}
Adapter 1: MidtransAdapter #
package adapter
import (
"fmt"
"time"
"myapp/midtrans"
"myapp/payment"
)
// MidtransAdapter mengimplementasikan payment.PaymentProcessor
// dengan mendelegasikan semua panggilan ke Midtrans SDK.
type MidtransAdapter struct {
client *midtrans.Client
paymentType string // "credit_card", "gopay", "bank_transfer", dll
}
func NewMidtransAdapter(client *midtrans.Client, paymentType string) payment.PaymentProcessor {
return &MidtransAdapter{
client: client,
paymentType: paymentType,
}
}
// Pay mengonversi panggilan PaymentProcessor.Pay() ke Midtrans Charge().
// Ini adalah inti dari adapter — translasi antara dua "bahasa" yang berbeda.
func (a *MidtransAdapter) Pay(amount int, currency, description string) (*payment.TransactionResult, error) {
req := midtrans.ChargeRequest{
PaymentType: a.paymentType,
TransactionID: fmt.Sprintf("order-%d", time.Now().UnixNano()),
GrossAmount: int64(amount),
Description: description,
}
resp, err := a.client.Charge(req)
if err != nil {
return nil, fmt.Errorf("midtrans charge failed: %w", err)
}
// Translasi response Midtrans ke TransactionResult standar
return &payment.TransactionResult{
TransactionID: resp.TransactionID,
Amount: amount,
Currency: currency,
Method: "midtrans/" + a.paymentType,
ProcessedAt: time.Now(),
RedirectURL: resp.RedirectURL,
}, nil
}
func (a *MidtransAdapter) Refund(transactionID string, amount int) error {
// Midtrans menggunakan "cancel" untuk refund
return a.client.CancelTransaction(transactionID)
}
func (a *MidtransAdapter) CheckStatus(transactionID string) (string, error) {
resp, err := a.client.GetTransactionStatus(transactionID)
if err != nil {
return "", fmt.Errorf("midtrans status check failed: %w", err)
}
// Translasi status Midtrans ke status standar
return normalizeMidtransStatus(resp.TransactionStatus), nil
}
func (a *MidtransAdapter) ProviderName() string {
return "Midtrans"
}
// normalizeMidtransStatus mengonversi status spesifik Midtrans ke status standar.
func normalizeMidtransStatus(midtransStatus string) string {
switch midtransStatus {
case "capture", "settlement":
return "success"
case "pending":
return "pending"
case "cancel", "expire":
return "failed"
default:
return "unknown"
}
}
Adapter 2: XenditAdapter #
package adapter
import (
"fmt"
"time"
"myapp/xendit"
"myapp/payment"
)
// XenditAdapter mengimplementasikan payment.PaymentProcessor
// dengan mendelegasikan semua panggilan ke Xendit SDK.
type XenditAdapter struct {
client *xendit.Client
callbackURL string
successURL string
}
func NewXenditAdapter(client *xendit.Client, callbackURL, successURL string) payment.PaymentProcessor {
return &XenditAdapter{
client: client,
callbackURL: callbackURL,
successURL: successURL,
}
}
// Pay mengonversi panggilan PaymentProcessor.Pay() ke Xendit CreateInvoice().
// Xendit berbasis invoice — beda konsep dari Midtrans yang berbasis charge.
func (a *XenditAdapter) Pay(amount int, currency, description string) (*payment.TransactionResult, error) {
req := xendit.InvoiceRequest{
ExternalID: fmt.Sprintf("inv-%d", time.Now().UnixNano()),
Amount: float64(amount),
Description: description,
Currency: currency,
CallbackURL: a.callbackURL,
SuccessURL: a.successURL,
}
resp, err := a.client.CreateInvoice(req)
if err != nil {
return nil, fmt.Errorf("xendit create invoice failed: %w", err)
}
return &payment.TransactionResult{
TransactionID: resp.ID,
Amount: amount,
Currency: currency,
Method: "xendit/invoice",
ProcessedAt: time.Now(),
RedirectURL: resp.InvoiceURL, // Xendit selalu butuh redirect ke invoice page
}, nil
}
func (a *XenditAdapter) Refund(transactionID string, amount int) error {
// Xendit menggunakan "expire" untuk membatalkan invoice
return a.client.ExpireInvoice(transactionID)
}
func (a *XenditAdapter) CheckStatus(transactionID string) (string, error) {
resp, err := a.client.GetInvoice(transactionID)
if err != nil {
return "", fmt.Errorf("xendit status check failed: %w", err)
}
return normalizeXenditStatus(resp.Status), nil
}
func (a *XenditAdapter) ProviderName() string {
return "Xendit"
}
func normalizeXenditStatus(xenditStatus string) string {
switch xenditStatus {
case "PAID", "SETTLED":
return "success"
case "PENDING":
return "pending"
case "EXPIRED":
return "failed"
default:
return "unknown"
}
}
Client Code: Bersih Tanpa Tahu Provider #
package service
import (
"fmt"
"myapp/payment"
)
// OrderService adalah client — hanya tahu interface PaymentProcessor.
// Tidak ada satu pun nama "Midtrans" atau "Xendit" di sini.
type OrderService struct {
processor payment.PaymentProcessor
}
func NewOrderService(processor payment.PaymentProcessor) *OrderService {
return &OrderService{processor: processor}
}
func (s *OrderService) Checkout(orderID string, amount int) (*payment.TransactionResult, error) {
result, err := s.processor.Pay(amount, "IDR", fmt.Sprintf("Payment for order %s", orderID))
if err != nil {
return nil, fmt.Errorf("checkout failed for order %s: %w", orderID, err)
}
fmt.Printf("Payment processed via %s: %s\n", s.processor.ProviderName(), result.TransactionID)
return result, nil
}
func (s *OrderService) ProcessRefund(transactionID string, amount int) error {
if err := s.processor.Refund(transactionID, amount); err != nil {
return fmt.Errorf("refund failed for transaction %s: %w", transactionID, err)
}
fmt.Printf("Refund processed via %s\n", s.processor.ProviderName())
return nil
}
func (s *OrderService) CheckPaymentStatus(transactionID string) (string, error) {
return s.processor.CheckStatus(transactionID)
}
Perakitan di main.go — hanya di sini provider konkret disebutkan:
func main() {
// Pilih provider berdasarkan konfigurasi — satu-satunya tempat provider disebutkan
var processor payment.PaymentProcessor
switch cfg.PaymentProvider {
case "midtrans":
midtransClient := midtrans.NewClient(cfg.MidtransServerKey, cfg.MidtransBaseURL)
processor = adapter.NewMidtransAdapter(midtransClient, "credit_card")
case "xendit":
xenditClient := xendit.NewClient(cfg.XenditAPIKey)
processor = adapter.NewXenditAdapter(xenditClient, cfg.CallbackURL, cfg.SuccessURL)
default:
log.Fatalf("unknown payment provider: %s", cfg.PaymentProvider)
}
// OrderService tidak tahu provider mana yang aktif
orderSvc := service.NewOrderService(processor)
result, err := orderSvc.Checkout("ORD-001", 150000)
// ...
}
Adapter untuk Migrasi Sistem Bertahap #
Salah satu use case terpenting Adapter Pattern adalah memungkinkan migrasi dari sistem lama ke sistem baru secara bertahap, tanpa big-bang rewrite yang berisiko.
sequenceDiagram
participant C as Client (kode baru)
participant A as LegacyAdapter
participant L as Legacy System
Note over C,L: Fase 1: Adapter menjembatani kode baru ke sistem lama
C->>A: Notify(recipient, message)
A->>L: SendEmail(to, subject, body)
L-->>A: response lama
A-->>C: error standar
Note over C,L: Fase 2: Saat sistem baru siap, ganti adapter — client tidak berubah
C->>A: Notify(recipient, message)
Note over A: adapter baru — langsung ke sistem baru
A-->>C: error standar
// LegacyEmailSystem adalah sistem lama yang tidak bisa diubah
type LegacyEmailSystem struct {
smtpServer string
smtpPort int
}
func (l *LegacyEmailSystem) SendEmail(to, subject, body, fromName, fromEmail string) int {
// Mengembalikan status code integer — bukan error
fmt.Printf("Sending via legacy SMTP to %s\n", to)
return 200
}
func (l *LegacyEmailSystem) GetQueueSize() int {
return 0
}
// Target interface — kontrak sistem baru
type Notifier interface {
Notify(recipient, message string) error
}
// LegacyEmailAdapter membungkus sistem lama agar kompatibel dengan Notifier
type LegacyEmailAdapter struct {
legacy *LegacyEmailSystem
}
func NewLegacyEmailAdapter(legacy *LegacyEmailSystem) Notifier {
return &LegacyEmailAdapter{legacy: legacy}
}
func (a *LegacyEmailAdapter) Notify(recipient, message string) error {
// Translasi: Notifier.Notify() → LegacyEmailSystem.SendEmail()
// Termasuk translasi return type: int → error
statusCode := a.legacy.SendEmail(
recipient,
"Notification", // subject default — sistem lama selalu butuh subject
message,
"System", // fromName default
"[email protected]",
)
if statusCode != 200 {
return fmt.Errorf("legacy email failed with status code: %d", statusCode)
}
return nil
}
// Saat sistem baru siap, buat adapter baru yang menunjuk ke implementasi baru.
// Tidak ada kode client yang berubah.
type ModernEmailAdapter struct {
modernClient *modernmail.Client
}
func NewModernEmailAdapter(client *modernmail.Client) Notifier {
return &ModernEmailAdapter{modernClient: client}
}
func (a *ModernEmailAdapter) Notify(recipient, message string) error {
return a.modernClient.Send(recipient, message)
}
Testing Adapter #
Testing Adapter berfokus pada dua hal: memastikan translasi parameter benar, dan memastikan translasi response/error benar. Karena Adapter mendelegasikan ke Adaptee, kamu bisa mock Adaptee untuk test yang terisolasi.
// MockMidtransClient untuk testing MidtransAdapter tanpa koneksi ke Midtrans
type MockMidtransClient struct {
ChargeFunc func(req midtrans.ChargeRequest) (*midtrans.ChargeResponse, error)
CancelTransactionFunc func(transactionID string) error
GetTransactionStatusFn func(transactionID string) (*midtrans.ChargeResponse, error)
}
func (m *MockMidtransClient) Charge(req midtrans.ChargeRequest) (*midtrans.ChargeResponse, error) {
return m.ChargeFunc(req)
}
func (m *MockMidtransClient) CancelTransaction(id string) error {
return m.CancelTransactionFunc(id)
}
func (m *MockMidtransClient) GetTransactionStatus(id string) (*midtrans.ChargeResponse, error) {
return m.GetTransactionStatusFn(id)
}
func TestMidtransAdapter_Pay_Success(t *testing.T) {
mockClient := &MockMidtransClient{
ChargeFunc: func(req midtrans.ChargeRequest) (*midtrans.ChargeResponse, error) {
// Verifikasi parameter yang dikirim ke Midtrans
if req.GrossAmount != 150000 {
t.Errorf("expected GrossAmount 150000, got %d", req.GrossAmount)
}
return &midtrans.ChargeResponse{
TransactionID: "mid-txn-123",
TransactionStatus: "capture",
}, nil
},
}
adapter := adapter.NewMidtransAdapter(mockClient, "credit_card")
result, err := adapter.Pay(150000, "IDR", "Test payment")
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
if result.TransactionID != "mid-txn-123" {
t.Errorf("expected TransactionID 'mid-txn-123', got %q", result.TransactionID)
}
if result.Method != "midtrans/credit_card" {
t.Errorf("unexpected method: %s", result.Method)
}
}
func TestMidtransAdapter_CheckStatus_TranslatesStatus(t *testing.T) {
tests := []struct {
midtransStatus string
expectedStatus string
}{
{"capture", "success"},
{"settlement", "success"},
{"pending", "pending"},
{"cancel", "failed"},
{"expire", "failed"},
{"unknown_status", "unknown"},
}
for _, tt := range tests {
t.Run(tt.midtransStatus, func(t *testing.T) {
mockClient := &MockMidtransClient{
GetTransactionStatusFn: func(id string) (*midtrans.ChargeResponse, error) {
return &midtrans.ChargeResponse{TransactionStatus: tt.midtransStatus}, nil
},
}
adpt := adapter.NewMidtransAdapter(mockClient, "credit_card")
status, err := adpt.CheckStatus("txn-123")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if status != tt.expectedStatus {
t.Errorf("for midtrans status %q: expected %q, got %q",
tt.midtransStatus, tt.expectedStatus, status)
}
})
}
}
Aturan Penempatan Adapter #
Adapter adalah kode infrastruktur, bukan kode bisnis. Penempatan yang tepat menjaga domain tetap bersih.
lib/
├── domain/
│ ├── payment/
│ │ └── processor.go ← Target interface di sini (domain/port)
│ └── notification/
│ └── notifier.go ← Target interface di sini
│
├── infrastructure/ ← Adapter selalu di sini
│ ├── payment/
│ │ ├── midtrans_adapter.go
│ │ ├── xendit_adapter.go
│ │ └── stripe_adapter.go
│ └── notification/
│ ├── twilio_adapter.go
│ └── sendgrid_adapter.go
│
└── main.go ← Perakitan adapter + injection
Adapter dan Hexagonal Architecture
Adapter Pattern adalah fondasi dari Hexagonal Architecture (juga dikenal sebagai Ports & Adapters). Interface di domain layer adalah “port” — kontrak yang mendefinisikan apa yang dibutuhkan domain. Adapter di infrastructure layer adalah “adapter” — implementasi konkret yang menghubungkan port ke dunia luar. Memahami Adapter Pattern berarti sudah memahami setengah dari Hexagonal Architecture.
Adapter vs Pattern Structural Lainnya #
Adapter sering dikacaukan dengan Facade dan Decorator karena ketiganya “membungkus” sesuatu. Perbedaannya terletak pada tujuan pembungkusan.
| Pattern | Tujuan | Interface Output |
|---|---|---|
| Adapter | Mengonversi interface yang tidak kompatibel | Berbeda dari yang dibungkus |
| Facade | Menyederhanakan subsistem yang kompleks | Berbeda dari yang dibungkus, tapi lebih sederhana |
| Decorator | Menambah behavior tanpa mengubah interface | Sama dengan yang dibungkus |
| Proxy | Mengontrol akses ke objek | Sama dengan yang dibungkus |
// Adapter: interface OUTPUT berbeda dari interface Adaptee
type LegacyGateway interface { MakePayment(total int) error }
type PaymentProcessor interface { Pay(amount int, currency string) error }
// LegacyGateway → PaymentProcessor: dua interface berbeda
// Decorator: interface OUTPUT sama dengan yang dibungkus
type PaymentProcessor interface { Pay(amount int, currency string) error }
// LoggingDecorator implements PaymentProcessor, wraps PaymentProcessor
// Sama interface, behavior tambahan
// Facade: menyembunyikan kompleksitas, bukan mengonversi
type PaymentFacade struct { /* sembunyikan auth, retry, logging */ }
func (f *PaymentFacade) SimpleCheckout(amount int) error { /* sederhanakan */ }
Kapan Menggunakan dan Kapan Tidak #
GUNAKAN Adapter jika:
✓ Ingin menggunakan library pihak ketiga dengan interface yang berbeda
✓ Sedang migrasi dari sistem lama ke sistem baru secara bertahap
✓ Perlu menyatukan beberapa provider berbeda di balik satu interface
✓ Ingin mengisolasi kode bisnis dari perubahan API eksternal
✓ Perlu membuat komponen lama bisa di-test dengan mock
HINDARI Adapter jika:
✗ Kamu mengontrol kedua sisi kode — lebih baik refactor langsung
✗ Interface yang perlu "diadaptasi" terlalu lebar (banyak method) — mungkin perlu Facade
✗ Adapter berisi logika bisnis — itu bukan tanggung jawab adapter
✗ Hanya ada satu provider dan tidak ada rencana mengganti — over-engineering
Checklist Review Adapter #
DESAIN:
□ Target interface didefinisikan di domain/bisnis layer, bukan di infrastructure
□ Adapter hanya berisi translasi — tidak ada business logic
□ Client bergantung pada interface, bukan concrete adapter
IMPLEMENTASI:
□ Adapter mengimplementasikan seluruh method Target interface
□ Error dari Adaptee di-wrap dengan konteks yang informatif (fmt.Errorf + %w)
□ Return type dikonversi ke tipe domain standar (bukan tipe SDK pihak ketiga)
□ Status/kode vendor-specific ditranslasikan ke status standar domain
PENEMPATAN:
□ Adapter berada di infrastructure layer, bukan domain layer
□ Import ke library pihak ketiga hanya ada di adapter — bukan di domain
□ Perakitan adapter (injection) dilakukan di main atau composition root
TESTING:
□ Adaptee di-mock untuk test yang terisolasi
□ Translasi parameter diverifikasi (nilai yang dikirim ke Adaptee sudah benar)
□ Translasi response dan error diverifikasi
□ Status mapping di-test untuk semua kemungkinan nilai vendor
Ringkasan #
- Adapter menjembatani dua interface yang tidak kompatibel — tanpa mengubah satu pun dari keduanya; hanya menambahkan lapisan tipis penerjemah di antara mereka.
- Tiga komponen kunci: Target interface (yang diharapkan client), Adaptee (yang sudah ada), dan Adapter (penerjemah antara keduanya).
- Di Golang selalu Object Adapter — komposisi, bukan inheritance; Adapter menyimpan referensi ke Adaptee dan mendelegasikan panggilan ke sana.
- Adapter hanya boleh berisi translasi — mapping method, konversi parameter, normalisasi status; jika ada business logic masuk ke sini, itu sinyal desain yang salah.
- Tempatkan di infrastructure layer — Target interface di domain layer, Adapter di infrastructure layer; ini menjaga domain bersih dari ketergantungan ke library eksternal.
- Fondasi Hexagonal Architecture — interface domain adalah “port”, Adapter di infrastructure adalah “adapter”; memahami Adapter Pattern berarti memahami inti dari arsitektur Ports & Adapters.
- Bedakan dari Decorator dan Facade: Adapter mengonversi interface yang berbeda; Decorator menambah behavior dengan interface yang sama; Facade menyederhanakan subsistem kompleks.
- Gunakan untuk migrasi bertahap — kode baru bisa berjalan di atas sistem lama melalui adapter, memberi waktu untuk migrasi tanpa big-bang rewrite.