Bridging Incompatible Interfaces with the Adapter Pattern
Structural Compatibility
The Adapter pattern serves as a bridge between two incompatible interfaces. This structural pattern allows classes with incompatible interfaces to collaborate without modifying their source code. It wraps an existing class (the adaptee) with a new interface (the target) that a client expects. This is particularly useful during system maintenance or integration when third-party libraries or legacy modules must be incorporated into a new architecture.
Practical Demonstration
Consider a presentation setup where an OldProjector requires a VGAInterface. The implementation of the legacy projector and its specific connector is shown below:
// Legacy Target Interface
interface VGAConnector {
void connectViaVGA(String signal);
}
// Legacy Projector Implementation
class LegacyProjector implements VGAConnector {
@Override
public void connectViaVGA(String signal) {
System.out.println("Legacy Projector receiving VGA signal: " + signal);
}
}
A modern laptop typically outputs video via an HDMIInterface. The implementation for the modern output standard differs from the legacy input:
// Modern Adaptee Interface
interface HDMIConnector {
void streamViaHDMI(String data);
}
// Modern Laptop Implementation
class ModernLaptop implements HDMIConnector {
@Override
public void streamViaHDMI(String data) {
System.out.println("Modern Laptop streaming HDMI data: " + data);
}
}
Attempting to connect the ModernLaptop directly to the LegacyProjector results in a type mismatch. To resolve this, we introduce an adapter that translates HDMI calls to VGA calls.
Class Adapter Implementation
The class adapter approach uses inheritance to achieve compatibility. The adapter inherits from the modern device (adaptee) and implements the legacy interface (target).
// Class Adapter
class HDMItoVGAClassAdapter extends ModernLaptop implements VGAConnector {
@Override
public void connectViaVGA(String signal) {
System.out.println("[Class Adapter] Converting signal...");
// The adapter translates the specific VGA connection request to the HDMI streaming method
streamViaHDMI(signal);
}
}
Object Adapter Implementation
Alternatively, the object adapter uses composition. This is generally preferred as it adheres to the principle of favoring composition over inheritance. The adapter holds a reference to the adaptee.
// Object Adapter
class HDMItoVGAObjectAdapter implements VGAConnector {
private HDMIConnector modernDevice;
public HDMItoVGAObjectAdapter(HDMIConnector device) {
this.modernDevice = device;
}
@Override
public void connectViaVGA(String signal) {
System.out.println("[Object Adapter] Translating protocol...");
modernDevice.streamViaHDMI(signal);
}
}
Two-Way Adapter
In scenarios where interoperability is required in both directions (e.g., connecting a legacy device to a modern screen), a two-way adapter can be implemented. This adapter implements both interfaces and manages references to both objects.
class BidirectionalAdapter implements VGAConnector, HDMIConnector {
private VGAConnector vgaDevice;
private HDMIConnector hdmiDevice;
public BidirectionalAdapter(VGAConnector vga) {
this.vgaDevice = vga;
}
public BidirectionalAdapter(HDMIConnector hdmi) {
this.hdmiDevice = hdmi;
}
@Override
public void connectViaVGA(String signal) {
System.out.println("[Bidirectional Adapter] HDMI to VGA");
if (hdmiDevice != null) {
hdmiDevice.streamViaHDMI(signal);
}
}
@Override
public void streamViaHDMI(String data) {
System.out.println("[Bidirectional Adapter] VGA to HDMI");
if (vgaDevice != null) {
vgaDevice.connectViaVGA(data);
}
}
}
Usage and Implementation Details
The following demonstrates how the object adapter resolves the incompatibility issue in the main execution flow:
public class PresentationSetup {
public static void main(String[] args) {
LegacyProjector oldProjector = new LegacyProjector();
ModernLaptop newLaptop = new ModernLaptop();
// Direct connection fails due to interface mismatch
// oldProjector.connectViaVGA(newLaptop); // Compile error
// Using Object Adapter to bridge the gap
HDMItoVGAObjectAdapter adapter = new HDMItoVGAObjectAdapter(newLaptop);
oldProjector.connectViaVGA("Presentation Data");
// Output via adapter triggers the HDMI stream
adapter.connectViaVGA("Presentation Data");
}
}
The primary distinction between class and object adapters lies in their flexibility. Object adapters allow the adaptation of multiple subclasses, whereas class adapters are restricted to a single specific adaptee due to the limitations of single inheritance in languages like Java. The two-way adapter extends the object adapter concept to support dual-directional translation, useful in complex integration scenarios involving heterogeneous systems.