Adapter Pattern: Adapting Systems From Legacy to Modern

The Adapter pattern converts the interface of a class into another interface clients expect, allowing classes with incompatible interfaces to work together.

The Adapter pattern is a structural design pattern that enables objects with incompatible interfaces to collaborate and work together. It acts as a bridge between two incompatible interfaces, converting the interface of one class into the expected interface by the client. This allows objects with different interfaces to interact seamlessly without modifying their existing code. The Adapter pattern is useful when integrating legacy systems, reusing existing classes, or incorporating third-party libraries that do not align with the desired interface. By employing the Adapter pattern, developers can promote code reusability, flexibility, and maintainability in their software systems.

Real World Analogy

When you travel from the US to Europe for the first time, you may get a surprise when trying to charge your laptop. The power plug and sockets standards are different in different countries. That’s why your US plug won’t fit a French socket. The problem can be solved by using a power plug adapter that has the American-style socket and the European-style plug.

When to Use Adapter Pattern

  1. Integrating with legacy systems: When you need to incorporate legacy code or systems into a new application that uses a different interface, the Adapter pattern can help bridge the gap by adapting the legacy interface to the new one.
  2. Reusing existing classes: If you have existing classes that provide valuable functionality but do not conform to the required interface, you can create adapters to make them compatible with other parts of the system without modifying their original code.
  3. Working with third-party libraries: When you want to utilize a third-party library that doesn’t match the interface or conventions of your application, creating an adapter allows you to seamlessly integrate the library into your codebase.
  4. Supporting multiple interfaces: In situations where an object needs to support multiple interfaces simultaneously, adapters can be created to translate between the object and different interfaces, enabling it to interact with various components of the system.
  5. Enhancing flexibility and maintainability: The Adapter pattern promotes loose coupling by decoupling the client code from the implementation details of the adapted class. This makes it easier to modify or replace the underlying implementation without affecting the client code.

Examples

Example 1 – Replacing Legacy Shape Library

Let’s consider a scenario where we have a legacy library that provides functionality to draw shapes, but it only supports drawing shapes in terms of x and y coordinates. However, we want to use this library in our modern application that works with a more advanced shape interface, which provides methods like draw() and resize().

To bridge the gap between the legacy library and our modern application, we can create an adapter class called ShapeAdapter that implements the modern shape interface and internally uses the legacy library to perform the shape drawing. Here’s an example implementation:

// Legacy shape library
class LegacyShapeLibrary {
    void drawShape(int x1, int y1, int x2, int y2) {
        System.out.println("Drawing shape at (" + x1 + "," + y1 + ") and (" + x2 + "," + y2 + ")");
    }
}

// Modern shape interface
interface Shape {
    void draw();
    void resize();
}

// Adapter class
class ShapeAdapter implements Shape {
    private LegacyShapeLibrary legacyShape;

    public ShapeAdapter(LegacyShapeLibrary legacyShape) {
        this.legacyShape = legacyShape;
    }

    @Override
    public void draw() {
        // Convert the modern draw() method to the legacy format
        legacyShape.drawShape(0, 0, 100, 100);
    }

    @Override
    public void resize() {
        // Implement the resize() method as per the modern interface
        System.out.println("Resizing the shape");
    }
}

// Usage example
public class Main {
    public static void main(String[] args) {
        LegacyShapeLibrary legacyShape = new LegacyShapeLibrary();
        ShapeAdapter shapeAdapter = new ShapeAdapter(legacyShape);

        // Use the adapter to draw and resize the shape
        shapeAdapter.draw();
        shapeAdapter.resize();
    }
}
Github
Adapter Pattern - Replacing Legacy Shape Library

Example 2 – Replacing Legacy Database Driver

Let’s assume that we have a Java application that needs to connect to a database to retrieve data. The application uses a third-party database driver library that only supports a specific interface for database access. However, we want to use our own database interface that provides more functionality and better fits our application’s needs.

To bridge the gap between the third-party library and our own database interface, we can create an adapter class called DatabaseAdapter that implements our database interface and internally uses the third-party library to perform the database operations. Here’s an example implementation:

// Third-party database driver library
interface ThirdPartyDatabaseDriver {
    void connect(String host, String user, String password);
    ResultSet query(String query);
    void disconnect();
}

// Our database interface
interface Database {
    Connection getConnection();
    ResultSet executeQuery(String query);
}

// Adapter class
class DatabaseAdapter implements Database {
    private ThirdPartyDatabaseDriver thirdPartyDriver;

    public DatabaseAdapter(ThirdPartyDatabaseDriver thirdPartyDriver) {
        this.thirdPartyDriver = thirdPartyDriver;
    }

    @Override
    public Connection getConnection() {
        // Convert the connect() method to the Connection object of our own database interface
        thirdPartyDriver.connect("localhost", "username", "password");
        return new Connection();
    }

    @Override
    public ResultSet executeQuery(String query) {
        // Convert the query() method to the executeQuery() method of our own database interface
        return thirdPartyDriver.query(query);
    }
}

// Our own Connection object
class Connection {
    // Implementation details
}

// Usage example
public class Main {
    public static void main(String[] args) {
        ThirdPartyDatabaseDriver thirdPartyDriver = new ThirdPartyDatabaseDriverImpl();
        DatabaseAdapter databaseAdapter = new DatabaseAdapter(thirdPartyDriver);

        // Use the adapter to connect to the database and execute queries
        Connection connection = databaseAdapter.getConnection();
        ResultSet result = databaseAdapter.executeQuery("SELECT * FROM users");
    }
}
Github
Adapter Pattern - Replacing Legacy Database Driver

JVM Usages

Potential Anti-Patterns

  • Overuse of adapters: Using adapters excessively can lead to an overly complex and convoluted codebase. If every interaction between components requires an adapter, it can introduce unnecessary layers of abstraction and decrease code readability and maintainability.
  • Premature use of the Adapter pattern: Applying the Adapter pattern too early in the development process can be unnecessary and add complexity to the codebase. It’s essential to carefully assess the need for adaptation and consider other alternatives before introducing adapters.
  • Adapting incompatible concepts: Adapting classes or interfaces that have fundamentally different concepts and behavior can lead to confusion and introduce subtle bugs. It’s important to ensure that the adaptation is feasible and meaningful, rather than forcing incompatible concepts to work together.
  • Lack of interface consistency: If adapters are created inconsistently or with varying interfaces, it can make the codebase difficult to understand and maintain. Adapters should adhere to consistent naming conventions and interface designs to ensure clarity and ease of use.
  • Neglecting performance considerations: Adapters can introduce additional overhead due to translation or mapping operations. It’s important to consider the performance implications of using adapters, especially in high-performance or resource-constrained systems.
  • Poorly designed adapter interfaces: Creating overly complex or bloated adapter interfaces can defeat the purpose of using adapters. Adapter interfaces should be minimal, focused, and provide only the necessary methods for interaction.

Read More: