Unified Interface Management with the Adapter Pattern
The Adapter Pattern is a structural design pattern that allows objects with incompatible interfaces to collaborate. While the classic pattern typically involves class adapters, object adapters, or interface adapters, this article explores a more generalized approach: a central adapter that manages and unifies the interaction with a diverse set of objects, each with its own unique interface.
The core idea is to create a single point of entry for client code, which can then work with various object types without needing to know their specific implementations. This decouples the client from the concrete classes, promoting flexibility and easier maintenance.
1. The Problem: Without an Adapter
Consider a scenario where we have different types of professionals, such as developers and educators. Each has its own distinct method for performing their work. Without a unifying mechanism, client code must be aware of each specific type to invoke the correct method.
Developer.java (The interface for developers)
public interface Developer {
void writeCode();
}
SoftwareDeveloper.java (An implementation of the Developer interface)
public class SoftwareDeveloper implements Developer {
@Override
public void writeCode() {
System.out.println("I am a software developer, and I am writing code.");
}
}
Educator.java (The interface for educators)
public interface Educator {
void instruct();
}
Teacher.java (A implementation of the Educator interface)
public class Teacher implements Educator {
@Override
public void instruct() {
System.out.println("I am a teacher, and I am instructing students.");
}
}
Main.java (The client code without an adapter)
public class Main {
public static void main(String[] args) {
Developer developer = new SoftwareDeveloper();
Educator educator = new Teacher();
// Client code must know the specific type of each object
developer.writeCode();
educator.instruct();
}
}
Output:
I am a software developer, and I am writing code.
I am a teacher, and I am instructing students.
Analysis: The client code is tightly coupled to the concrete classes (SoftwareDeveloper, Teacher). For every new professional type, the client code would need to be modified to handle it, violating the Open/Closed Principal.
2. Solution 1: A Single Unified Adapter
We can introduce a single adapter that handles all object types. This adapter will inspect the object's type and delegate the call to the appropriate method.
WorkHandler.java (The unified adapter interface)
public interface WorkHandler {
void handle(Object entity);
}
UnifiedWorkHandler.java (The implementation of the single adapter)
public class UnifiedWorkHandler implements WorkHandler {
@Override
public void handle(Object entity) {
if (entity instanceof Developer) {
((Developer) entity).writeCode();
} else if (entity instanceof Educator) {
((Educator) entity).instruct();
}
// Add more type checks as needed
}
}
Main.java (The client code using the single adapter)
public class Main {
public static void main(String[] args) {
Developer developer = new SoftwareDeveloper();
Educator educator = new Teacher();
Object[] professionals = {developer, educator};
WorkHandler handler = new UnifiedWorkHandler();
// The client code now interacts with a single interface
for (Object professional : professionals) {
handler.handle(professional);
}
}
}
Output:
I am a software developer, and I am writing code.
I am a teacher, and I am instructing students.
Analysis: The client code is now decoupled from the specific implementations. It only needs to know about the WorkHandler interface, making the system more maintainable and extensible.
3. Solution 2: Multiple Specialized Adapters
For a more scalable approach, we can define a separate adapter for each type. This follows the Single Responsibility Principle more closely. Each adapter is responsible for handling one specific type.
WorkHandler.java (The adapter interface, now with a support check)
public interface WorkHandler {
void handle(Object entity);
boolean supports(Object entity);
}
DeveloperHandler.java (Specialized adapter for developers)
public class DeveloperHandler implements WorkHandler {
@Override
public void handle(Object entity) {
((Developer) entity).writeCode();
}
@Override
public boolean supports(Object entity) {
return entity instanceof Developer;
}
}
EducatorHandler.java (Specialized adapter for educators)
public class EducatorHandler implements WorkHandler {
@Override
public void handle(Object entity) {
((Educator) entity).instruct();
}
@Override
public boolean supports(Object entity) {
return entity instanceof Educator;
}
}
Main.java (The client code using multiple adapters)
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
Developer developer = new SoftwareDeveloper();
Educator educator = new Teacher();
List<Object> professionals = new ArrayList<>();
professionals.add(developer);
professionals.add(educator);
// A list of all available adapters
List<WorkHandler> handlers = new ArrayList<>();
handlers.add(new DeveloperHandler());
handlers.add(new EducatorHandler());
// The client code iterates through objects and finds the correct handler
for (Object professional : professionals) {
WorkHandler handler = findHandler(professional, handlers);
if (handler != null) {
handler.handle(professional);
}
}
}
private static WorkHandler findHandler(Object entity, List<WorkHandler> handlers) {
for (WorkHandler handler : handlers) {
if (handler.supports(entity)) {
return handler;
}
}
return null; // Or throw an exception if no handler is found
}
}
Output:
I am a software developer, and I am writing code.
I am a teacher, and I am instructing students.
Analysis: This approach provides better separation of concerns. Each adapter is self-contained and responsible for one type. The findHandler method acts as a factory, selecting the appropriate adapter for a given object. This pattern is highly extensible; adding a new professional type only requires creating a new handler and adding it to the list, without modifying existing client code.