Adapter Pattern

The Adapter Pattern is a structural design pattern that acts as a bridge between two incompatible interfaces. Think of it as a universal power adapter for your code—it allows classes with incompatible interfaces to work together seamlessly, without modifying their source code.
Just like a USB-C to USB-A adapter lets you connect your new phone to an old charger, the Adapter Pattern enables legacy code to work with modern systems, or third-party libraries to integrate smoothly into your application.
The Problem
Imagine you're building a media player application. You have an existing MediaPlayer interface that plays MP3 files perfectly. Now you want to add support for MP4 and VLC formats, but you have third-party libraries with completely different interfaces. How do you integrate them without rewriting your entire codebase?
// Your existing interface
public interface MediaPlayer {
void play(String audioType, String fileName);
}
// Third-party library with different interface
public class AdvancedMediaPlayer {
public void playMp4(String fileName) {
System.out.println("Playing MP4: " + fileName);
}
public void playVlc(String fileName) {
System.out.println("Playing VLC: " + fileName);
}
}
// How do you make these work together? 🤔
The Solution: Adapter Pattern
The Adapter Pattern creates a wrapper that translates one interface into another. It acts as a middleman, converting requests from the client into a format the adaptee understands.
Key Components
Target: The interface that the client expects
Adapter: Converts the Target interface to the Adaptee interface
Adaptee: The existing class with an incompatible interface
Client: Uses the Target interface
Types of Adapters
1. Class Adapter (Using Inheritance)
2. Object Adapter (Using Composition)
Real-World Implementation
Example 1: Media Player Adapter
// Target Interface - What the client expects
public interface MediaPlayer {
void play(String audioType, String fileName);
}
// Adaptee - Third-party advanced player
public class AdvancedMediaPlayer {
public void playMp4(String fileName) {
System.out.println("Playing MP4 file: " + fileName);
}
public void playVlc(String fileName) {
System.out.println("Playing VLC file: " + fileName);
}
}
// Adapter - Bridges the gap
public class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedPlayer;
public MediaAdapter(String audioType) {
this.advancedPlayer = new AdvancedMediaPlayer();
}
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("mp4")) {
advancedPlayer.playMp4(fileName);
} else if (audioType.equalsIgnoreCase("vlc")) {
advancedPlayer.playVlc(fileName);
}
}
}
// Concrete Implementation
public class AudioPlayer implements MediaPlayer {
private MediaAdapter mediaAdapter;
@Override
public void play(String audioType, String fileName) {
// Built-in support for MP3
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing MP3 file: " + fileName);
}
// Use adapter for other formats
else if (audioType.equalsIgnoreCase("mp4") ||
audioType.equalsIgnoreCase("vlc")) {
mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType, fileName);
} else {
System.out.println("Invalid format: " + audioType);
}
}
}
// Client Code
public class AdapterPatternDemo {
public static void main(String[] args) {
AudioPlayer audioPlayer = new AudioPlayer();
audioPlayer.play("mp3", "song.mp3");
audioPlayer.play("mp4", "video.mp4");
audioPlayer.play("vlc", "movie.vlc");
audioPlayer.play("avi", "film.avi");
}
}
/* Output:
Playing MP3 file: song.mp3
Playing MP4 file: video.mp4
Playing VLC file: movie.vlc
Invalid format: avi
*/
Example 2: Payment Gateway Adapter
// Target Interface
public interface PaymentProcessor {
void processPayment(double amount);
boolean validatePayment(String cardNumber);
}
// Adaptee 1 - PayPal (Third-party)
public class PayPalGateway {
public void makePayment(double amount) {
System.out.println("Processing $" + amount + " via PayPal");
}
public boolean checkCard(String card) {
System.out.println("Validating PayPal account: " + card);
return true;
}
}
// Adaptee 2 - Stripe (Third-party)
public class StripeGateway {
public void charge(double amount) {
System.out.println("Charging $" + amount + " via Stripe");
}
public boolean verifyCard(String cardNumber) {
System.out.println("Verifying card with Stripe: " + cardNumber);
return true;
}
}
// Adapter for PayPal
public class PayPalAdapter implements PaymentProcessor {
private PayPalGateway payPal;
public PayPalAdapter() {
this.payPal = new PayPalGateway();
}
@Override
public void processPayment(double amount) {
payPal.makePayment(amount);
}
@Override
public boolean validatePayment(String cardNumber) {
return payPal.checkCard(cardNumber);
}
}
// Adapter for Stripe
public class StripeAdapter implements PaymentProcessor {
private StripeGateway stripe;
public StripeAdapter() {
this.stripe = new StripeGateway();
}
@Override
public void processPayment(double amount) {
stripe.charge(amount);
}
@Override
public boolean validatePayment(String cardNumber) {
return stripe.verifyCard(cardNumber);
}
}
// Client Code
public class PaymentDemo {
public static void main(String[] args) {
PaymentProcessor paypal = new PayPalAdapter();
paypal.validatePayment("user@paypal.com");
paypal.processPayment(99.99);
System.out.println();
PaymentProcessor stripe = new StripeAdapter();
stripe.validatePayment("4111-1111-1111-1111");
stripe.processPayment(149.99);
}
}
Workflow Diagram
Two-Way Adapter
Sometimes you need bidirectional adaptation:
// Target Interface A
public interface ModernPrinter {
void printDocument(String content);
}
// Target Interface B
public interface LegacyPrinter {
void print(String text);
}
// Two-Way Adapter
public class PrinterAdapter implements ModernPrinter, LegacyPrinter {
private ModernPrinter modernPrinter;
private LegacyPrinter legacyPrinter;
public PrinterAdapter(ModernPrinter modern) {
this.modernPrinter = modern;
}
public PrinterAdapter(LegacyPrinter legacy) {
this.legacyPrinter = legacy;
}
@Override
public void printDocument(String content) {
if (legacyPrinter != null) {
legacyPrinter.print(content);
}
}
@Override
public void print(String text) {
if (modernPrinter != null) {
modernPrinter.printDocument(text);
}
}
}
Real-World Use Cases
When to Use the Adapter Pattern
✅ Use When
Legacy System Integration: You need to use an old class but its interface doesn't match your needs
Third-Party Libraries: External libraries have incompatible interfaces
Reusability: You want to reuse existing classes that don't fit your interface
Multiple Incompatible Interfaces: Several classes need to work together but have different interfaces
Interface Standardization: You want to create a common interface for similar classes
❌ Avoid When
Simple Refactoring: You can easily modify the source code directly
Over-Engineering: The interfaces are already compatible
Performance Critical: The extra layer adds unnecessary overhead
Single Use Case: You're only using it once and won't reuse it
Adapter vs Other Patterns
| Pattern | Purpose | Interface Change | Use Case |
|---|---|---|---|
| Adapter | Make incompatible interfaces work | Changes | Legacy integration, third-party |
| Decorator | Add responsibilities dynamically | Keeps same | Add features without subclassing |
| Facade | Simplify complex subsystems | Simplifies | Provide simple API to complex code |
| Proxy | Control access to an object | Keeps same | Lazy loading, access control |
Advanced Example: Database Adapter
// Target Interface
public interface Database {
void connect(String url);
void executeQuery(String query);
void disconnect();
}
// Adaptee 1 - MySQL
public class MySQLDatabase {
public void connectToMySQL(String host, int port, String db) {
System.out.println("Connected to MySQL: " + host + ":" + port + "/" + db);
}
public void runQuery(String sql) {
System.out.println("Executing MySQL query: " + sql);
}
public void closeConnection() {
System.out.println("MySQL connection closed");
}
}
// Adaptee 2 - MongoDB
public class MongoDBDatabase {
public void establishConnection(String connectionString) {
System.out.println("Connected to MongoDB: " + connectionString);
}
public void find(String collection, String filter) {
System.out.println("Finding in " + collection + " with filter: " + filter);
}
public void terminate() {
System.out.println("MongoDB connection terminated");
}
}
// MySQL Adapter
public class MySQLAdapter implements Database {
private MySQLDatabase mysql;
public MySQLAdapter() {
this.mysql = new MySQLDatabase();
}
@Override
public void connect(String url) {
// Parse URL: mysql://localhost:3306/mydb
String[] parts = url.split("://")[1].split("/");
String[] hostPort = parts[0].split(":");
mysql.connectToMySQL(hostPort[0], Integer.parseInt(hostPort[1]), parts[1]);
}
@Override
public void executeQuery(String query) {
mysql.runQuery(query);
}
@Override
public void disconnect() {
mysql.closeConnection();
}
}
// MongoDB Adapter
public class MongoDBAdapter implements Database {
private MongoDBDatabase mongo;
public MongoDBAdapter() {
this.mongo = new MongoDBDatabase();
}
@Override
public void connect(String url) {
mongo.establishConnection(url);
}
@Override
public void executeQuery(String query) {
// Convert SQL-like query to MongoDB format
mongo.find("collection", query);
}
@Override
public void disconnect() {
mongo.terminate();
}
}
// Database Factory
public class DatabaseFactory {
public static Database getDatabase(String type) {
switch (type.toLowerCase()) {
case "mysql":
return new MySQLAdapter();
case "mongodb":
return new MongoDBAdapter();
default:
throw new IllegalArgumentException("Unknown database type: " + type);
}
}
}
// Client Code
public class DatabaseDemo {
public static void main(String[] args) {
// Use MySQL
Database db1 = DatabaseFactory.getDatabase("mysql");
db1.connect("mysql://localhost:3306/mydb");
db1.executeQuery("SELECT * FROM users");
db1.disconnect();
System.out.println();
// Use MongoDB with same interface
Database db2 = DatabaseFactory.getDatabase("mongodb");
db2.connect("mongodb://localhost:27017/mydb");
db2.executeQuery("{name: 'John'}");
db2.disconnect();
}
}
Benefits
Single Responsibility Principle: Separates interface conversion from business logic
Open/Closed Principle: Add new adapters without modifying existing code
Flexibility: Easily swap implementations
Reusability: Reuse existing classes without modification
Decoupling: Client code doesn't depend on concrete adaptee classes
Drawbacks
Complexity: Adds extra layer of abstraction
Performance: Slight overhead from additional method calls
Maintenance: More classes to maintain
Over-Adaptation: Can lead to too many adapter classes
Best Practices
Use Object Adapter Over Class Adapter: Composition is more flexible than inheritance
Keep Adapters Simple: Don't add business logic to adapters
Document Adaptations: Clearly document what's being adapted and why
Consider Facade: If adapting multiple classes, consider Facade pattern instead
Test Thoroughly: Ensure adapter correctly translates all operations
Version Compatibility: Handle different versions of adaptee classes gracefully
Conclusion
The Adapter Pattern is your Swiss Army knife for interface incompatibility. Whether you're integrating legacy systems, working with third-party libraries, or standardizing diverse interfaces, the Adapter Pattern provides a clean, maintainable solution without modifying existing code.
Remember: Don't force a square peg into a round hole—use an adapter! 🔌
🎯 Key Takeaway
The Adapter Pattern is all about compatibility without modification. When you can't change the source but need to make it work, wrap it in an adapter!
An Adapter and a Decorator walked into a bar. The bartender said, "What'll it be?" The Adapter replied, "I'll have whatever he's having, but make it compatible with my interface!" 🍺 ⚡�
Happy Adapting! 🔌✨




