Adapter Pattern: Make Incompatible Interfaces Work
Learn: Adapter Pattern: Make Incompatible Interfaces Work
Welcome to TopperBlog! π
I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.
π― What I Write About:
β’ AI/ML Engineering & LLMs
β’ Web3 & Blockchain Development
β’ System Design & Architecture
β’ Interview Preparation (FAANG)
β’ Freelancing & Remote Work
β’ Modern Tech Stacks (Next.js, React, Rust, TypeScript)
β’ Performance Optimization & Best Practices
πΌ Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.
π 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.
π Let's connect and grow together in this amazing tech journey!
#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering
Adapter Pattern: Make Incompatible Interfaces Work
Overview
The Adapter Pattern is a structural design pattern that allows objects with incompatible interfaces to collaborate. It acts as a bridge between two incompatible interfaces, converting the interface of a class into another interface clients expect.
Problem
Scenario
Imagine you're building a payment processing system. Your application uses a PaymentProcessor interface that expects:
process(amount: number): boolean
However, you need to integrate with a third-party payment gateway (LegacyPaymentGateway) that has a completely different interface:
executeTransaction(value: number, currency: string): {success: boolean, transactionId: string}
Challenges:
- You cannot modify the third-party library code
- Your application code expects the
PaymentProcessorinterface - Direct integration would require changing all client code
- Multiple incompatible interfaces need to work together seamlessly
- Code becomes tightly coupled and difficult to maintain
Why It's a Problem
- Interface Mismatch: The third-party library doesn't conform to your expected interface
- Rigidity: Changing client code to accommodate different interfaces violates the Open/Closed Principle
- Maintainability: Multiple integration points become scattered and hard to manage
- Reusability: Cannot easily swap implementations without refactoring
Solution
Concept
Create an Adapter class that:
- Implements the interface your application expects
- Wraps the incompatible object (adaptee)
- Translates method calls from the expected interface to the adaptee's interface
- Returns results in the format your application expects
Benefits
- Decoupling: Client code remains independent of third-party interfaces
- Flexibility: Easy to add new adapters for different incompatible interfaces
- Reusability: Adapters can be reused across different parts of the application
- Maintainability: Changes to third-party libraries only affect the adapter
- Single Responsibility: Each adapter handles one specific incompatibility
Structure
βββββββββββββββββββ
β Client β
ββββββββββ¬βββββββββ
β uses
βΌ
βββββββββββββββββββ ββββββββββββββββββββ
β Target βββββββββββ Adapter β
β Interface β adapts β β
βββββββββββββββββββ β ββββββββββββββββ β
β β Adaptee β β
β β (Incompatible)
β ββββββββββββββββ β
ββββββββββββββββββββ
Code Implementation
1. Class Adapter Pattern (Inheritance)
// Target Interface - What the client expects
interface PaymentProcessor {
process(amount: number): boolean;
}
// Adaptee - Third-party library with incompatible interface
class LegacyPaymentGateway {
executeTransaction(
value: number,
currency: string
): { success: boolean; transactionId: string } {
// Simulating legacy payment processing
console.log(`Processing ${value} ${currency} via legacy gateway`);
return {
success: value > 0,
transactionId: `TXN-${Date.now()}`,
};
}
}
// Adapter - Makes LegacyPaymentGateway compatible with PaymentProcessor
class PaymentGatewayAdapter
extends LegacyPaymentGateway
implements PaymentProcessor
{
process(amount: number): boolean {
const result = this.executeTransaction(amount, "USD");
console.log(`Adapter: Converted process() to executeTransaction()`);
return result.success;
}
}
// Client Code
class CheckoutService {
constructor(private paymentProcessor: PaymentProcessor) {}
checkout(amount: number): void {
if (this.paymentProcessor.process(amount)) {
console.log("β Payment successful");
} else {
console.log("β Payment failed");
}
}
}
// Usage
const adapter = new PaymentGatewayAdapter();
const checkout = new CheckoutService(adapter);
checkout.checkout(100); // Works seamlessly!
2. Object Adapter Pattern (Composition)
// Target Interface
interface DataSource {
read(): string;
write(data: string): void;
}
// Adaptee - Incompatible third-party library
class LegacyDatabase {
getData(): string {
return "Legacy data format";
}
saveData(info: string): void {
console.log(`Saving to legacy DB: ${info}`);
}
}
// Adapter using composition
class DatabaseAdapter implements DataSource {
constructor(private legacyDb: LegacyDatabase) {}
read(): string {
const legacyData = this.legacyDb.getData();
// Transform legacy format to modern format
return `[MODERN] ${legacyData}`;
}
write(data: string): void {
// Transform modern format to legacy format
const legacyFormat = data.replace("[MODERN]", "");
this.legacyDb.saveData(legacyFormat);
}
}
// Client Code
class ApplicationService {
constructor(private dataSource: DataSource) {}
loadAndProcess(): void {
const data = this.dataSource.read();
console.log(`Processing: ${data}`);
this.dataSource.write(`${data} - processed`);
}
}
// Usage
const legacyDb = new LegacyDatabase();
const adapter = new DatabaseAdapter(legacyDb);
const app = new ApplicationService(adapter);
app.loadAndProcess();
3. Real-World Example: Media Player
// Target Interface
interface MediaPlayer {
play(filename: string): void;
}
// Adaptees - Different media formats
class VLCPlayer {
playVLC(filename: string): void {
console.log(`VLC playing: ${filename}`);
}
}
class QuickTimePlayer {
playQuickTime(filename: string): void {
console.log(`QuickTime playing: ${filename}`);
}
}
// Adapters for each format
class VLCAdapter implements MediaPlayer {
constructor(private vlcPlayer: VLCPlayer) {}
play(filename: string): void {
if (filename.endsWith(".avi")) {
this.vlcPlayer.playVLC(filename);
}
}
}
class QuickTimeAdapter implements MediaPlayer {
constructor(private qtPlayer: QuickTimePlayer) {}
play(filename: string): void {
if (filename.endsWith(".mov")) {
this.qtPlayer.playQuickTime(filename);
}
}
}
// Client - Works with any adapter
class AudioPlayer {
private players: Map<string, MediaPlayer> = new Map();
registerPlayer(format: string, player: MediaPlayer): void {
this.players.set(format, player);
}
playMedia(filename: string): void {
const extension = filename.split(".").pop() || "";
const player = this.players.get(extension);
if (player) {
player.play(filename);
} else {
console.log(`No player found for ${extension}`);
}
}
}
// Usage
const audioPlayer = new AudioPlayer();
audioPlayer.registerPlayer("avi", new VLCAdapter(new VLCPlayer()));
audioPlayer.registerPlayer("mov", new QuickTimeAdapter(new QuickTimePlayer()));
audioPlayer.playMedia("movie.avi"); // VLC playing: movie.avi
audioPlayer.playMedia("video.mov"); // QuickTime playing: video.mov
4. Advanced Example: Multi-Adapter System
// Target Interface
interface Logger {
log(level: string, message: string): void;
}
// Multiple Adaptees
class ConsoleLogger {
output(msg: string): void {
console.log(`[CONSOLE] ${msg}`);
}
}
class FileLogger {
writeToFile(filename: string, content: string): void {
console.log(`[FILE] Writing to ${filename}: ${content}`);
}
}
class RemoteLogger {
sendToServer(endpoint: string, payload: object): void {
console.log(`[REMOTE] Sending to ${endpoint}:`, payload);
}
}
// Adapters
class ConsoleLoggerAdapter implements Logger {
constructor(private logger: ConsoleLogger) {}
log(level: string, message: string): void {
this.logger.output(`[${level}] ${message}`);
}
}
class FileLoggerAdapter implements Logger {
constructor(private logger: FileLogger) {}
log(level: string, message: string): void {
this.logger.writeToFile("app.log", `[${level}] ${message}`);
}
}
class RemoteLoggerAdapter implements Logger {
constructor(private logger: RemoteLogger) {}
log(level: string, message: string): void {
this.logger.sendToServer("/api/logs", {
level,
message,
timestamp: new Date().toISOString(),
});
}
}
// Composite Logger using adapters
class CompositeLogger implements Logger {
private loggers: Logger[] = [];
addLogger(logger: Logger): void {
this.loggers.push(logger);
}
log(level: string, message: string): void {
this.loggers.forEach((logger) => logger.log(level, message));
}
}
// Usage
const compositeLogger = new CompositeLogger();
compositeLogger.addLogger(new ConsoleLoggerAdapter(new ConsoleLogger()));
compositeLogger.addLogger(new FileLoggerAdapter(new FileLogger()));
compositeLogger.addLogger(new RemoteLoggerAdapter(new RemoteLogger()));
compositeLogger.log("ERROR", "Database connection failed");
// Logs to console, file, and remote server simultaneously
Key Characteristics
| Aspect | Details |
| Type | Structural Pattern |
| Purpose | Convert incompatible interfaces |
| Participants | Client, Target, Adapter, Adaptee |
| Complexity | Low to Medium |
| Use Cases | Legacy integration, third-party libraries, format conversion |
| Variants | Class Adapter (inheritance), Object Adapter (composition) |
When to Use
β Use Adapter Pattern when:
- Integrating third-party libraries with incompatible interfaces
- Working with legacy code that cannot be modified
- Need to make multiple incompatible interfaces work together
- Want to decouple client code from specific implementations
- Creating a unified interface for different implementations
β Avoid when:
- You can modify the incompatible interface directly
- The adaptation logic is too complex (consider Facade instead)
- Performance is critical and extra layers matter
- Only adapting a single interface (might be over-engineering)
Comparison with Similar Patterns
| Pattern | Purpose | Key Difference |
| Adapter | Make incompatible interfaces compatible | Works with existing interfaces |
| Bridge | Decouple abstraction from implementation | Designed upfront for flexibility |
| Facade | Simplify complex subsystems | Provides simplified interface |
| Decorator | Add behavior dynamically | Enhances existing interface |
Conclusion
The Adapter Pattern is essential for integrating incompatible systems without modifying existing code. It promotes loose coupling, enhances maintainability, and provides a clean way to handle interface mismatches. By using adapters, you create a flexible architecture that can easily accommodate new integrations and changes to third-party dependencies.