Skip to main content

Command Palette

Search for a command to run...

Adapter Pattern

Updated
9 min read
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

  1. Target: The interface that the client expects

  2. Adapter: Converts the Target interface to the Adaptee interface

  3. Adaptee: The existing class with an incompatible interface

  4. 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

  1. Legacy System Integration: You need to use an old class but its interface doesn't match your needs

  2. Third-Party Libraries: External libraries have incompatible interfaces

  3. Reusability: You want to reuse existing classes that don't fit your interface

  4. Multiple Incompatible Interfaces: Several classes need to work together but have different interfaces

  5. Interface Standardization: You want to create a common interface for similar classes

❌ Avoid When

  1. Simple Refactoring: You can easily modify the source code directly

  2. Over-Engineering: The interfaces are already compatible

  3. Performance Critical: The extra layer adds unnecessary overhead

  4. 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

  1. Single Responsibility Principle: Separates interface conversion from business logic

  2. Open/Closed Principle: Add new adapters without modifying existing code

  3. Flexibility: Easily swap implementations

  4. Reusability: Reuse existing classes without modification

  5. Decoupling: Client code doesn't depend on concrete adaptee classes

Drawbacks

  1. Complexity: Adds extra layer of abstraction

  2. Performance: Slight overhead from additional method calls

  3. Maintenance: More classes to maintain

  4. Over-Adaptation: Can lead to too many adapter classes

Best Practices

  1. Use Object Adapter Over Class Adapter: Composition is more flexible than inheritance

  2. Keep Adapters Simple: Don't add business logic to adapters

  3. Document Adaptations: Clearly document what's being adapted and why

  4. Consider Facade: If adapting multiple classes, consider Facade pattern instead

  5. Test Thoroughly: Ensure adapter correctly translates all operations

  6. 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! 🔌✨