Adapter Pattern
The Adapter Pattern converts the interface of a class into another interface that clients expect. It allows classes to work together that couldn't otherwise because of incompatible interfaces.
Think of it like a power adapter that lets you plug a device from one country into an outlet in another country - it changes the interface but preserves the underlying functionality.
Problem
Imagine you have an existing system with a well-defined interface. Now you want to use another library or system that has a different interface. You can't change either system's code, but you need them to work together.
Components
- Target: The interface that the client expects and uses.
- Adapter: The class that bridges between the Target and Adaptee.
- Adaptee: The class with an incompatible interface that needs to be adapted.
- Client: The class that interacts with objects through the Target interface.
Implementation Example
Let's look at a real-world example of the Adapter pattern: adapting a legacy payment system to work with a new e-commerce platform.
// Target Interface: What our e-commerce system expects
class ModernPaymentProcessor {
processPayment(amount) {
// Process the payment
}
}
// Adaptee: Legacy payment system with a different interface
class LegacyPaymentSystem {
constructor() {
this.status = 'initialized';
}
initialize() {
this.status = 'ready';
return true;
}
makePayment(dollars, cents) {
if (this.status !== 'ready') {
return false;
}
console.log(`Legacy payment processed: $${dollars}.${cents}`);
return true;
}
shutdown() {
this.status = 'closed';
}
}
// Adapter: Makes LegacyPaymentSystem work with our expected interface
class PaymentSystemAdapter extends ModernPaymentProcessor {
constructor(legacySystem) {
super();
this.legacySystem = legacySystem;
this.legacySystem.initialize();
}
processPayment(amount) {
// Split the amount into dollars and cents for the legacy system
const dollars = Math.floor(amount);
const cents = Math.round((amount - dollars) * 100);
// Call the adaptee using its interface
const result = this.legacySystem.makePayment(dollars, cents);
if (!result) {
throw new Error('Payment failed');
}
return {
success: true,
amount: amount,
timestamp: new Date()
};
}
}
Interactive Demo: Payment System Adapter
Try processing payments using the adapter pattern, which connects our new system to the legacy payment processor.
When to Use
- When you want to use an existing class, but its interface doesn't match what you need
- When you need to use several existing subclasses, but it's impractical to adapt their interface by subclassing every one
- When you want to create a reusable class that cooperates with classes that don't necessarily have compatible interfaces
Benefits
- Allows incompatible interfaces to work together
- Reuses existing functionality without changing its code
- Provides a clean separation between client code and the adapted class
- Applies the Single Responsibility Principle by separating interface conversion from the core business logic
Real-World Uses
- Integrating legacy systems with new applications
- Using third-party libraries with incompatible interfaces
- Converting data formats (like an XML to JSON adapter)
- Implementing compatibility layers for hardware devices