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.

← Sebelumnya: Prototype   Berikutnya: Bridge →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact