Design Principle
Adapter Pattern
Learn the Adapter pattern: allow incompatible interfaces to work together. Convert interface of a class into another interface clients expect.
The Adapter Pattern allows objects with incompatible interfaces to collaborate. It acts as a bridge between two incompatible interfaces by wrapping an object to make it compatible with another interface.
What is the Adapter Pattern?
Adapter Pattern provides:
- Interface conversion: Converts one interface to another
- Compatibility: Makes incompatible interfaces work together
- Legacy integration: Integrates legacy code with new systems
- Reusability: Reuses existing classes with different interfaces
Use cases:
- Integrating third-party libraries
- Working with legacy code
- Converting data formats
- Making incompatible classes work together
Structure
Client
└─ uses Target interface
Target (interface)
└─ request()
Adapter (implements Target)
└─ adaptee: Adaptee
└─ request() (calls adaptee.specificRequest())
Adaptee (existing class)
└─ specificRequest()
Examples
Object Adapter
// Target interface (what client expects)
interface MediaPlayer {
play(audioType: string, fileName: string): void;
}
// Adaptee (existing class with incompatible interface)
class AdvancedMediaPlayer {
playVlc(fileName: string): void {
console.log(`Playing VLC file: ${fileName}`);
}
playMp4(fileName: string): void {
console.log(`Playing MP4 file: ${fileName}`);
}
}
// Adapter (adapts Adaptee to Target)
class MediaAdapter implements MediaPlayer {
private advancedPlayer: AdvancedMediaPlayer;
constructor(audioType: string) {
this.advancedPlayer = new AdvancedMediaPlayer();
}
play(audioType: string, fileName: string): void {
if (audioType === "vlc") {
this.advancedPlayer.playVlc(fileName);
} else if (audioType === "mp4") {
this.advancedPlayer.playMp4(fileName);
}
}
}
// Client
class AudioPlayer implements MediaPlayer {
play(audioType: string, fileName: string): void {
if (audioType === "mp3") {
console.log(`Playing MP3 file: ${fileName}`);
} else if (audioType === "vlc" || audioType === "mp4") {
const adapter = new MediaAdapter(audioType);
adapter.play(audioType, fileName);
} else {
console.log(`Invalid media type: ${audioType}`);
}
}
}
// Usage
const player = new AudioPlayer();
player.play("mp3", "song.mp3");
player.play("vlc", "movie.vlc");
player.play("mp4", "video.mp4");
Class Adapter (Multiple Inheritance)
// Target
interface Target {
request(): string;
}
// Adaptee
class Adaptee {
specificRequest(): string {
return "Adaptee's specific request";
}
}
// Adapter (extends Adaptee, implements Target)
class Adapter extends Adaptee implements Target {
request(): string {
return this.specificRequest();
}
}
// Usage
const adapter = new Adapter();
console.log(adapter.request());
Payment Gateway Adapter
// Target interface
interface PaymentProcessor {
processPayment(amount: number, currency: string): void;
}
// Legacy payment system (Adaptee)
class LegacyPaymentSystem {
pay(amount: number): void {
console.log(`Legacy payment: $${amount}`);
}
}
// New payment system (Adaptee)
class ModernPaymentSystem {
charge(amount: number, currency: string): void {
console.log(`Modern payment: ${amount} ${currency}`);
}
}
// Adapter for legacy system
class LegacyPaymentAdapter implements PaymentProcessor {
private legacySystem: LegacyPaymentSystem;
constructor() {
this.legacySystem = new LegacyPaymentSystem();
}
processPayment(amount: number, currency: string): void {
// Convert currency and call legacy method
const usdAmount = this.convertToUSD(amount, currency);
this.legacySystem.pay(usdAmount);
}
private convertToUSD(amount: number, currency: string): number {
// Currency conversion logic
const rates: { [key: string]: number } = {
"EUR": 1.1,
"GBP": 1.3,
"USD": 1.0
};
return amount * (rates[currency] || 1.0);
}
}
// Adapter for modern system
class ModernPaymentAdapter implements PaymentProcessor {
private modernSystem: ModernPaymentSystem;
constructor() {
this.modernSystem = new ModernPaymentSystem();
}
processPayment(amount: number, currency: string): void {
this.modernSystem.charge(amount, currency);
}
}
// Client
class PaymentService {
private processor: PaymentProcessor;
constructor(processor: PaymentProcessor) {
this.processor = processor;
}
makePayment(amount: number, currency: string): void {
this.processor.processPayment(amount, currency);
}
}
// Usage
const legacyAdapter = new LegacyPaymentAdapter();
const paymentService1 = new PaymentService(legacyAdapter);
paymentService1.makePayment(100, "EUR");
const modernAdapter = new ModernPaymentAdapter();
const paymentService2 = new PaymentService(modernAdapter);
paymentService2.makePayment(100, "USD");
Common Pitfalls
- Over-adapting: Creating adapters for everything. Fix: Only use when interfaces are truly incompatible
- Performance overhead: Adapter adds layer. Fix: Consider performance impact
- Complex adapters: Adapters become too complex. Fix: Keep adapters simple, single responsibility
- Tight coupling: Adapter tightly coupled to adaptee. Fix: Use interfaces, dependency injection
Interview Questions
Beginner
Q: What is the Adapter pattern and when would you use it?
A:
Adapter Pattern allows objects with incompatible interfaces to work together.
How it works:
- Wraps incompatible object: Adapter wraps adaptee
- Implements target interface: Adapter implements interface client expects
- Translates calls: Converts calls from target to adaptee
Example:
// Client expects this interface
interface Target {
request(): void;
}
// Existing class has incompatible interface
class Adaptee {
specificRequest(): void { /* ... */ }
}
// Adapter makes them compatible
class Adapter implements Target {
private adaptee: Adaptee;
request(): void {
this.adaptee.specificRequest();
}
}
Use cases:
- Legacy integration: Integrate legacy code with new systems
- Third-party libraries: Use libraries with incompatible interfaces
- Data format conversion: Convert between different data formats
- API integration: Integrate different APIs
Intermediate
Q: Explain the difference between Object Adapter and Class Adapter. What are the trade-offs?
A:
Object Adapter (Composition):
Uses composition to adapt:
class Adapter implements Target {
private adaptee: Adaptee; // Composition
request(): void {
this.adaptee.specificRequest();
}
}
Advantages:
- Flexible: Can adapt multiple adaptees
- Single responsibility: Adapter only adapts
- No inheritance: Doesn't inherit adaptee's interface
Disadvantages:
- Extra object: Additional object in memory
- Indirection: Extra method call
Class Adapter (Inheritance):
Uses inheritance to adapt:
class Adapter extends Adaptee implements Target {
request(): void {
this.specificRequest(); // Inherited method
}
}
Advantages:
- Direct access: Direct access to adaptee methods
- No extra object: No additional object
- Efficient: Slightly more efficient
Disadvantages:
- Multiple inheritance: Requires multiple inheritance (not in all languages)
- Tight coupling: Tightly coupled to adaptee
- Less flexible: Can only adapt one adaptee
When to use:
- Object Adapter: Preferred in most cases (more flexible)
- Class Adapter: When you need direct access, language supports multiple inheritance
Senior
Q: Design an adapter system for integrating multiple payment gateways (Stripe, PayPal, Square) with a unified payment interface. Handle different response formats, error handling, and transaction status mapping.
A:
// Unified payment interface
interface PaymentGateway {
processPayment(amount: number, currency: string, card: CardInfo): Promise<PaymentResult>;
refund(transactionId: string, amount: number): Promise<RefundResult>;
getTransactionStatus(transactionId: string): Promise<TransactionStatus>;
}
// Stripe adapter
class StripeAdapter implements PaymentGateway {
private stripe: StripeClient;
constructor(apiKey: string) {
this.stripe = new StripeClient(apiKey);
}
async processPayment(amount: number, currency: string, card: CardInfo): Promise<PaymentResult> {
try {
// Convert to Stripe format
const stripeCharge = {
amount: Math.round(amount * 100), // Convert to cents
currency: currency.toLowerCase(),
source: {
number: card.number,
exp_month: card.expMonth,
exp_year: card.expYear,
cvc: card.cvv
}
};
const response = await this.stripe.charges.create(stripeCharge);
// Convert Stripe response to unified format
return {
success: true,
transactionId: response.id,
amount: response.amount / 100,
currency: response.currency.toUpperCase(),
status: this.mapStatus(response.status)
};
} catch (error) {
return {
success: false,
error: this.mapError(error)
};
}
}
async refund(transactionId: string, amount: number): Promise<RefundResult> {
const refund = await this.stripe.refunds.create({
charge: transactionId,
amount: Math.round(amount * 100)
});
return {
success: true,
refundId: refund.id
};
}
async getTransactionStatus(transactionId: string): Promise<TransactionStatus> {
const charge = await this.stripe.charges.retrieve(transactionId);
return this.mapStatus(charge.status);
}
private mapStatus(stripeStatus: string): TransactionStatus {
const statusMap: { [key: string]: TransactionStatus } = {
"succeeded": "completed",
"pending": "pending",
"failed": "failed"
};
return statusMap[stripeStatus] || "unknown";
}
private mapError(error: any): string {
return error.message || "Payment processing failed";
}
}
// PayPal adapter
class PayPalAdapter implements PaymentGateway {
private paypal: PayPalClient;
constructor(clientId: string, secret: string) {
this.paypal = new PayPalClient(clientId, secret);
}
async processPayment(amount: number, currency: string, card: CardInfo): Promise<PaymentResult> {
try {
// Convert to PayPal format
const payment = {
intent: "sale",
payer: {
payment_method: "credit_card",
funding_instruments: [{
credit_card: {
number: card.number,
type: this.mapCardType(card.type),
expire_month: card.expMonth,
expire_year: card.expYear,
cvv2: card.cvv
}
}]
},
transactions: [{
amount: {
total: amount.toString(),
currency: currency
}
}]
};
const response = await this.paypal.payment.create(payment);
// Convert PayPal response to unified format
return {
success: response.state === "approved",
transactionId: response.id,
amount: parseFloat(response.transactions[0].amount.total),
currency: response.transactions[0].amount.currency,
status: this.mapStatus(response.state)
};
} catch (error) {
return {
success: false,
error: this.mapError(error)
};
}
}
// Similar methods for refund and status...
private mapCardType(type: string): string {
const typeMap: { [key: string]: string } = {
"visa": "visa",
"mastercard": "mastercard",
"amex": "amex"
};
return typeMap[type] || "visa";
}
private mapStatus(paypalState: string): TransactionStatus {
const statusMap: { [key: string]: TransactionStatus } = {
"approved": "completed",
"pending": "pending",
"failed": "failed"
};
return statusMap[paypalState] || "unknown";
}
private mapError(error: any): string {
return error.details?.[0]?.description || "Payment processing failed";
}
}
// Payment service using adapters
class PaymentService {
private gateway: PaymentGateway;
constructor(gateway: PaymentGateway) {
this.gateway = gateway;
}
async processPayment(amount: number, currency: string, card: CardInfo): Promise<PaymentResult> {
return await this.gateway.processPayment(amount, currency, card);
}
}
// Usage
const stripeAdapter = new StripeAdapter("sk_test_...");
const paymentService = new PaymentService(stripeAdapter);
const result = await paymentService.processPayment(100, "USD", cardInfo);
Features:
- Unified interface: Same interface for all gateways
- Format conversion: Converts between formats
- Error mapping: Maps errors to unified format
- Status mapping: Maps statuses to unified format
Key Takeaways
- Adapter pattern: Makes incompatible interfaces work together
- Object adapter: Uses composition (preferred)
- Class adapter: Uses inheritance (when supported)
- Use cases: Legacy integration, third-party libraries, API integration
- Benefits: Reusability, compatibility, integration
- Best practices: Keep adapters simple, use interfaces, handle errors properly
Keep exploring
Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.