Adapter

Structural Design Patterns

Imagine your application processes data using a consistent, modern JSON format. You have a standard interface, IJsonProcessor, that all your data-handling services use. Now, your company acquires a new, super-fast analytics library that could dramatically improve performance. There's just one problem: this new library is older and only accepts data in XML format.

// Your application's standard interface
public interface IJsonProcessor {
    void processJson(String jsonData);
}

// The new, incompatible library you need to use
public class SuperFastXmlAnalytics {
    public void analyzeXmlData(String xmlData) {
        // ... complex, high-performance XML analysis
        System.out.println("Analyzing XML data...");
    }
}

You can't change the third-party library's code. You also don't want to rewrite your entire application to work with XML, as that would be a massive, risky effort and pollute your clean architecture. How do you make these two incompatible interfaces work together?

Making Interfaces Compatible

The Adapter (or a Wrapper) is a structural design pattern that allows objects with incompatible interfaces to collaborate.

It acts like a real-world power adapter. If you have an Indian laptop charger but you're in Vietnam, you don't throw away the charger or rewire the wall socket. You use a simple adapter that plugs into the Vietnamese socket on one end and accepts the Indian plug on the other. The Adapter pattern does the exact same thing for software objects, acting as a middle-man that translates requests from one interface to another.

The Participants

The pattern involves a few key roles:

  1. Target: The interface that your existing client code expects to work with (our IJsonProcessor).
  2. Client: The class that uses the Target interface (e.g., your main Application).
  3. Adaptee: The existing class with the incompatible interface that you need to adapt (the SuperFastXmlAnalytics library).
  4. Adapter: The class that bridges the gap. It implements the Target interface and holds a reference to an instance of the Adaptee.

The flow is simple: The Client calls a method on the Adapter using the Target interface. The Adapter receives this call, performs any necessary data conversion (e.g., JSON to XML), and then delegates the call to the wrapped Adaptee object.

The Analytics Adapter

Let's build an adapter to make our SuperFastXmlAnalytics library compatible with our application.

public class AnalyticsAdapter implements IJsonProcessor {
    // 1. The Adapter holds a reference to the adaptee.
    private final SuperFastXmlAnalytics analyticsLibrary;

    public AnalyticsAdapter(SuperFastXmlAnalytics analyticsLibrary) {
        this.analyticsLibrary = analyticsLibrary;
    }

    // 2. The Adapter implements the target interface.
    @Override
    public void processJson(String jsonData) {
        System.out.println("Adapter is converting JSON to XML...");
        // In a real application, this would be a proper conversion.
        String xmlData = convertJsonToXml(jsonData); 

        // 3. The Adapter delegates the call to the adaptee.
        analyticsLibrary.analyzeXmlData(xmlData);
    }

    private String convertJsonToXml(String jsonData) {
        // A dummy conversion for demonstration
        return "<data>" + jsonData + "</data>";
    }
}

Now, the client code can use the new library without changing at all. It still only knows about the IJsonProcessor interface.

// In our main Application class...
String jsonData = "{'user':'Jasir'}";

IJsonProcessor processor = new AnalyticsAdapter(new SuperFastXmlAnalytics());
processor.processJson(jsonData);

// Output:
// Adapter is converting JSON to XML...
// Analyzing XML data...

We've successfully integrated the new library without modifying any existing client code.

Interview Focus: Analysis and Trade-offs

Benefits:

  • Promotes Reusability: You can reuse existing classes and third-party components even if their interfaces don't align with your current architecture.
  • Adheres to SOLID Principles:
    • Single Responsibility Principle: The complex logic of data conversion and interfacing with the adaptee is isolated within the adapter class, keeping both the client and the adaptee clean.
    • Open/Closed Principle: Your client code is "closed" for modification but "open" to being extended with new adapters to support other analytics libraries in the future.

Drawbacks:

  • Increased Complexity: The pattern introduces an extra layer of indirection and a new class for each adaptation, which can add complexity to the system.

Object vs. Class Adapters: The example above uses the Object Adapter pattern (via composition), where the adapter holds an instance of the adaptee. This is by far the most common and flexible approach. An alternative, the Class Adapter, uses multiple inheritance. Since Java doesn't support multiple class inheritance, this is less common and less flexible, so you should focus on the Object Adapter for interviews.

How Adapter Relates to Other Concepts

The Adapter pattern is a fundamental tool for system integration and maintaining clean architectural boundaries.

  • Connecting Incompatible Systems: Its primary role is to act as a translator between different parts of a system, especially when dealing with legacy code or external APIs that you cannot change.
  • Comparison with Other "Wrapper" Patterns:
    • Decorator: Also a wrapper, but its purpose is to add new responsibilities to an object while keeping the same interface.
    • Facade: A Facade's goal is to provide a simpler, unified interface to a complex subsystem. It's about reducing complexity, not about fixing incompatibility.
    • Proxy: A Proxy has the exact same interface as the object it wraps. Its purpose is to control access to that object (e.g., for lazy loading, logging, or security checks), not to change the interface.