Menguasai Adapter Pattern: Jurus Rahasia Menjembatani Inkompatibilitas di Aplikasi Web Modern
1. Pendahuluan
Pernahkah Anda berhadapan dengan situasi di mana Anda memiliki dua komponen atau sistem yang hebat, tapi mereka tidak bisa “bicara” satu sama lain karena format atau interface mereka berbeda? Di dunia web development yang serba cepat dan penuh integrasi ini, masalah inkompatibilitas adalah hal yang sangat lumrah. Entah itu data dari API pihak ketiga yang formatnya aneh, komponen UI yang membutuhkan props dalam bentuk tertentu, atau bahkan library lama yang harus bekerja dengan kode modern.
Di sinilah Adapter Pattern datang sebagai pahlawan. Pola desain ini memungkinkan dua interface yang tadinya tidak kompatibel untuk bekerja sama. Bayangkan seperti travel adapter universal yang Anda gunakan saat bepergian ke luar negeri; colokan laptop Anda mungkin standar Indonesia, tapi listrik di negara lain punya bentuk colokan yang berbeda. Travel adapter mengubah bentuk colokan Anda agar kompatibel dengan stop kontak di negara tujuan, tanpa harus mengubah laptop atau stop kontaknya.
Dalam artikel ini, kita akan menyelami Adapter Pattern, melihat bagaimana ia bekerja, dan yang paling penting, bagaimana kita bisa menggunakannya secara praktis di aplikasi web modern, baik di sisi frontend maupun backend. Mari kita ubah frustrasi inkompatibilitas menjadi fleksibilitas dan keterpaduan!
2. Memahami Adapter Pattern
Secara garis besar, Adapter Pattern adalah pola desain struktural yang memungkinkan objek dengan interface yang tidak kompatibel untuk berkolaborasi. Ini dilakukan dengan membuat objek perantara, yang disebut “Adapter”, yang membungkus salah satu objek dan mengubah interface-nya agar sesuai dengan yang diharapkan oleh objek lain.
Ada empat elemen utama dalam Adapter Pattern:
- Target (Antarmuka Target): Interface yang diharapkan oleh Client. Ini adalah “bahasa” yang ingin dipahami oleh Client.
- Client (Klien): Objek yang ingin menggunakan fungsionalitas tertentu, tetapi hanya tahu interface Target.
- Adaptee (Objek yang Diadaptasi): Objek yang memiliki fungsionalitas yang dibutuhkan oleh Client, tetapi interface-nya tidak kompatibel dengan Target. Ini adalah objek yang akan “diadaptasi”.
- Adapter (Adaptor): Objek yang menjembatani Target dan Adaptee. Ia mengimplementasikan interface Target, membungkus objek Adaptee, dan menerjemahkan panggilan dari Client ke format yang bisa dipahami oleh Adaptee.
📌 Analogi Konkret: Bayangkan Anda memiliki charger ponsel lama dengan colokan USB-A, tetapi laptop Anda hanya memiliki port USB-C.
- Target: Port USB-C di laptop Anda (ia mengharapkan colokan USB-C).
- Client: Laptop Anda (ingin mengisi daya ponsel).
- Adaptee: Charger ponsel lama Anda (memiliki colokan USB-A).
- Adapter: Konverter USB-A ke USB-C (ini yang akan Anda beli dan colokkan ke laptop, lalu colokkan charger ponsel ke konverter ini).
Tanpa Adapter (konverter), laptop dan charger ponsel tidak bisa bekerja sama. Adapter memungkinkan mereka berinteraksi tanpa harus mengubah laptop atau charger ponsel Anda.
3. Adapter di Frontend: Menjembatani Komponen UI dan Data
Di frontend, Adapter Pattern sangat berguna untuk mengelola data dan komponen UI. Seringkali, data yang kita dapatkan dari backend (misalnya dari REST API) tidak langsung cocok dengan format props yang diharapkan oleh komponen UI kita, terutama jika komponen itu bersifat reusable atau bagian dari design system.
🎯 Contoh Praktis: Mengadaptasi Data API untuk Komponen Kartu Produk
Misalkan kita memiliki komponen ProductCard yang mengharapkan props dalam format yang rapi:
// interfaces/Product.ts
interface ProductCardProps {
id: string;
title: string;
imageUrl: string;
priceFormatted: string; // Harga sudah diformat
availabilityStatus: 'inStock' | 'outOfStock' | 'limited';
}
// components/ProductCard.tsx
import React from 'react';
const ProductCard: React.FC<ProductCardProps> = ({
id,
title,
imageUrl,
priceFormatted,
availabilityStatus,
}) => {
return (
<div className="product-card">
<img src={imageUrl} alt={title} className="product-image" />
<h3>{title}</h3>
<p className="price">{priceFormatted}</p>
<span className={`status ${availabilityStatus}`}>
{availabilityStatus === 'inStock' ? 'Tersedia' : 'Habis'}
</span>
{/* ... detail lainnya */}
</div>
);
};
export default ProductCard;
Namun, API backend kita mengembalikan data dalam format yang sedikit berbeda:
// interfaces/ApiProduct.ts
interface ApiProductResponse {
productId: string;
name: string;
pictureUrl: string;
currentPrice: number; // Harga mentah dalam angka
stockCount: number;
lastUpdated: string;
}
Daripada mengubah komponen ProductCard atau memanipulasi data langsung di tempat pemanggilan (yang bisa berantakan jika banyak komponen yang butuh format berbeda), kita bisa membuat Adapter:
// adapters/ProductAdapter.ts
import { ApiProductResponse } from '../interfaces/ApiProduct';
import { ProductCardProps } from '../interfaces/Product';
class ProductAdapter {
static adapt(apiProduct: ApiProductResponse): ProductCardProps {
const priceFormatted = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
}).format(apiProduct.currentPrice);
let availabilityStatus: ProductCardProps['availabilityStatus'];
if (apiProduct.stockCount > 10) {
availabilityStatus = 'inStock';
} else if (apiProduct.stockCount > 0) {
availabilityStatus = 'limited';
} else {
availabilityStatus = 'outOfStock';
}
return {
id: apiProduct.productId,
title: apiProduct.name,
imageUrl: apiProduct.pictureUrl,
priceFormatted,
availabilityStatus,
};
}
}
export default ProductAdapter;
Sekarang, saat kita mengambil data dari API, kita bisa mengadaptasinya dengan mudah:
// pages/ProductListing.tsx
import React, { useEffect, useState } from 'react';
import ProductCard from '../components/ProductCard';
import ProductAdapter from '../adapters/ProductAdapter';
import { ApiProductResponse } from '../interfaces/ApiProduct';
const ProductListing: React.FC = () => {
const [products, setProducts] = useState<ProductCardProps[]>([]);
useEffect(() => {
const fetchProducts = async () => {
// Simulasi fetch dari API
const apiResponse: ApiProductResponse[] = [
{
productId: 'prod-001',
name: 'Kemeja Batik Premium',
pictureUrl: 'https://example.com/batik.jpg',
currentPrice: 250000,
stockCount: 15,
lastUpdated: '2023-10-26T10:00:00Z',
},
{
productId: 'prod-002',
name: 'Celana Jeans Slim Fit',
pictureUrl: 'https://example.com/jeans.jpg',
currentPrice: 300000,
stockCount: 3,
lastUpdated: '2023-10-26T10:05:00Z',
},
// ... data lainnya
];
const adaptedProducts = apiResponse.map(ProductAdapter.adapt);
setProducts(adaptedProducts);
};
fetchProducts();
}, []);
return (
<div className="product-list">
{products.map((product) => (
<ProductCard key={product.id} {...product} />
))}
</div>
);
};
export default ProductListing;
✅ Manfaat di Frontend:
- Dekopling: Komponen UI tidak perlu tahu detail format data API.
- Fleksibilitas: Jika backend berubah format, hanya Adapter yang perlu diubah, bukan semua komponen UI.
- Keterbacaan: Logika transformasi data terisolasi di satu tempat.
4. Adapter di Backend: Integrasi API Eksternal dan Database
Di backend, Adapter Pattern bahkan lebih krusial, terutama dalam microservices atau sistem yang berinteraksi dengan banyak layanan eksternal atau database yang berbeda.
🎯 Contoh Praktis: Mengadaptasi Payment Gateway Pihak Ketiga
Bayangkan sistem e-commerce Anda menggunakan beberapa payment gateway (misalnya, Midtrans dan Xendit) yang memiliki interface API yang berbeda-beda untuk memproses pembayaran. Daripada menulis logika spesifik untuk setiap gateway di setiap tempat pembayaran dipicu, kita bisa menggunakan Adapter.
Pertama, definisikan interface Target kita:
// interfaces/PaymentGateway.ts
export interface PaymentRequest {
orderId: string;
amount: number;
currency: string;
customerEmail: string;
}
export interface PaymentResponse {
transactionId: string;
status: 'pending' | 'success' | 'failed';
message?: string;
}
export interface IPaymentGateway {
processPayment(request: PaymentRequest): Promise<PaymentResponse>;
refundPayment(transactionId: string, amount: number): Promise<PaymentResponse>;
}
Sekarang, kita punya Adaptee (misalnya, library Midtrans yang interface-nya berbeda):
// services/MidtransSdk.ts (simulasi library Midtrans)
export class MidtransSdk {
private apiKey: string;
constructor(apiKey: string) {
this.apiKey = apiKey;
}
// Metode asli dari Midtrans SDK
public async createTransaction(midtransReq: {
gross_amount: number;
order_id: string;
customer_details: { email: string };
}): Promise<{ transaction_id: string; status_code: string; message: string }> {
console.log(`[Midtrans] Creating transaction for order ${midtransReq.order_id}...`);
// Simulasi respons API Midtrans
return new Promise((resolve) => {
setTimeout(() => {
if (midtransReq.gross_amount > 1000000) { // Simulasi gagal untuk jumlah besar
resolve({ transaction_id: 'MT-' + Date.now(), status_code: '400', message: 'Payment failed due to large amount' });
} else {
resolve({ transaction_id: 'MT-' + Date.now(), status_code: '200', message: 'Success' });
}
}, 500);
});
}
public async cancelTransaction(transactionId: string): Promise<{ status: string }> {
console.log(`[Midtrans] Cancelling transaction ${transactionId}...`);
return new Promise((resolve) => {
setTimeout(() => resolve({ status: 'cancelled' }), 300);
});
}
}
Dan ini adalah Adapter untuk Midtrans:
// adapters/MidtransPaymentAdapter.ts
import { IPaymentGateway, PaymentRequest, PaymentResponse } from '../interfaces/PaymentGateway';
import { MidtransSdk } from '../services/MidtransSdk'; // Adaptee
export class MidtransPaymentAdapter implements IPaymentGateway {
private midtransSdk: MidtransSdk;
constructor(apiKey: string) {
this.midtransSdk = new MidtransSdk(apiKey);
}
async processPayment(request: PaymentRequest): Promise<PaymentResponse> {
const midtransReq = {
gross_amount: request.amount,
order_id: request.orderId,
customer_details: { email: request.customerEmail },
};
try {
const midtransRes = await this.midtransSdk.createTransaction(midtransReq);
return {
transactionId: midtransRes.transaction_id,
status: midtransRes.status_code === '200' ? 'success' : 'failed',
message: midtransRes.message,
};
} catch (error: any) {
return {
transactionId: '',
status: 'failed',
message: error.message || 'Unknown Midtrans error',
};
}
}
async refundPayment(transactionId: string, amount: number): Promise<PaymentResponse> {
// Midtrans SDK mungkin tidak memiliki metode refund yang persis
// Kita harus mengadaptasi atau membuat implementasi yang sesuai
// Untuk contoh ini, kita asumsikan ada metode cancel yang bisa digunakan sebagai refund sederhana
try {
await this.midtransSdk.cancelTransaction(transactionId);
return {
transactionId,
status: 'success', // Asumsi cancel berarti refund berhasil
message: `Refund for ${transactionId} processed via Midtrans`,
};
} catch (error: any) {
return {
transactionId,
status: '