Adapter Pattern
The Adapter pattern (also known as Wrapper) allows objects with incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces by wrapping one interface to match another.
Key Concepts
- Target Interface - The interface your application expects
- Adaptee - The existing class with an incompatible interface
- Adapter - Wraps the adaptee and implements the target interface
- Translation - Converts calls from target interface to adaptee's interface
Interactive Demo
See how adapters allow different payment gateways to work with the same checkout system:
Select Payment Method:
Transaction Log
// Select a payment method and click Process Payment
Implementation Examples
Basic User API Adapter
// Incompatible Legacy API
class LegacyUserAPI {
getUserInfo(userId) {
return {
user_id: userId,
first_name: 'John',
last_name: 'Doe',
email_address: 'john.doe@example.com',
registration_date: '2023-01-15'
};
}
}
// Modern Application expects different interface
class ModernUserDisplay {
render(user) {
// Expects: { id, fullName, email, joinDate }
return `
<div>
<h3>${user.fullName}</h3>
<p>ID: ${user.id}</p>
<p>Email: ${user.email}</p>
<p>Joined: ${user.joinDate}</p>
</div>
`;
}
}
// Adapter bridges the gap
class UserAPIAdapter {
constructor(legacyAPI) {
this.legacyAPI = legacyAPI;
}
getUser(userId) {
const legacyData = this.legacyAPI.getUserInfo(userId);
// Transform legacy format to modern format
return {
id: legacyData.user_id,
fullName: `${legacyData.first_name} ${legacyData.last_name}`,
email: legacyData.email_address,
joinDate: new Date(legacyData.registration_date).toLocaleDateString()
};
}
}
// Usage
const legacyAPI = new LegacyUserAPI();
const adapter = new UserAPIAdapter(legacyAPI);
const display = new ModernUserDisplay();
const user = adapter.getUser(123);
display.render(user); // Works perfectly! Payment Gateway Adapters
// Different Payment Gateways with Different Interfaces
class PayPalGateway {
sendPayment(amount, email) {
return {
status: 'success',
transaction_id: 'PP-' + Math.random().toString(36).substr(2, 9),
amount: amount,
payee: email
};
}
}
class StripeGateway {
charge(cents, token) {
return {
success: true,
id: 'ch_' + Math.random().toString(36).substr(2, 9),
amount_charged: cents,
payment_method: token
};
}
}
// Target interface our app expects
class PaymentAdapter {
processPayment(amount, paymentDetails) {
throw new Error('Must be implemented by subclass');
}
}
// Adapter for PayPal
class PayPalAdapter extends PaymentAdapter {
constructor() {
super();
this.gateway = new PayPalGateway();
}
processPayment(amount, paymentDetails) {
const result = this.gateway.sendPayment(amount, paymentDetails.email);
return {
success: result.status === 'success',
transactionId: result.transaction_id,
amount: result.amount
};
}
}
// Adapter for Stripe
class StripeAdapter extends PaymentAdapter {
constructor() {
super();
this.gateway = new StripeGateway();
}
processPayment(amount, paymentDetails) {
const cents = amount * 100;
const result = this.gateway.charge(cents, paymentDetails.token);
return {
success: result.success,
transactionId: result.id,
amount: result.amount_charged / 100
};
}
}
// Now we can use either payment method with the same interface
function checkout(adapter, amount, details) {
const result = adapter.processPayment(amount, details);
console.log(`Payment ${result.success ? 'succeeded' : 'failed'}`);
return result;
} Data Format Adapter (XML to JSON)
// Adapting different data formats
class XMLDataService {
fetchData() {
return `
<users>
<user id="1">
<name>Alice</name>
<role>Admin</role>
</user>
<user id="2">
<name>Bob</name>
<role>User</role>
</user>
</users>
`;
}
}
class JSONAdapter {
constructor(xmlService) {
this.xmlService = xmlService;
}
getData() {
const xmlString = this.xmlService.fetchData();
// Simple XML to JSON conversion (in real app, use DOMParser)
const parser = new DOMParser();
const doc = parser.parseFromString(xmlString, 'text/xml');
const users = Array.from(doc.querySelectorAll('user'));
return users.map(user => ({
id: user.getAttribute('id'),
name: user.querySelector('name').textContent,
role: user.querySelector('role').textContent
}));
}
} Common Use Cases
- Third-Party Integration - Adapt external APIs to match your interface
- Legacy Code - Use old code with new systems without modifying it
- Data Format Conversion - Transform XML to JSON, CSV to objects, etc.
- Multiple Implementations - Support different backends with same interface
- Testing - Create mock adapters for different services
Benefits
- Single Responsibility - Separates interface conversion from business logic
- Open/Closed Principle - Add new adapters without modifying existing code
- Reusability - Adapt legacy or third-party code without changing it
- Flexibility - Swap implementations easily
- Testability - Mock adapters for unit testing
Trade-offs
- Complexity - Adds extra layer of abstraction
- Performance - Additional method calls and data transformation overhead
- Code Volume - Requires creating adapter classes for each incompatible interface
- Discovery - Can be harder to understand the flow with multiple adapters
Adapter vs Facade vs Decorator
- Adapter - Changes an interface to match another (interface conversion)
- Facade - Simplifies a complex interface (simplification)
- Decorator - Adds functionality to an object (enhancement)