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?
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 pattern involves a few key roles:
IJsonProcessor).Target interface (e.g., your main Application).SuperFastXmlAnalytics library).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.
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.
Benefits:
Drawbacks:
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.
The Adapter pattern is a fundamental tool for system integration and maintaining clean architectural boundaries.