Adapter & Bridge Patterns
We now move into Structural Patterns, which explain how to assemble objects and classes into larger structures while keeping these structures flexible and efficient.
1. The Adapter Pattern
The Problem
You have an existing class (Legacy or 3rd-party) that provides the functionality you need, but its interface doesn’t match the one your application uses. You cannot change the legacy code. This is common when integrating new libraries into an existing codebase.
The Solution
The Adapter acts as a wrapper between two objects. It catches calls for one object and transforms them to format and interface recognizable by the second object.
Class Adapter vs. Object Adapter
There are two ways to implement this:
- Object Adapter (Composition): The adapter contains an instance of the legacy class. This is the most flexible approach and is used in the example below.
- Class Adapter (Inheritance): The adapter inherits from both the target interface and the legacy class. This requires multiple inheritance, which is not supported in all languages (like Java).
Python Example (Object Adapter)
# Target Interface (what our app expects)
class ModernUSB:
def connect_usb_c(self):
pass
# Adaptee (existing/legacy component)
class OldMemoryStick:
def plug_in_usb_a(self):
return "Connected to old USB-A port."
# Adapter
class USBAdapter(ModernUSB):
def __init__(self, old_stick: OldMemoryStick):
self.old_stick = old_stick
def connect_usb_c(self):
# Translate the call
return self.old_stick.plug_in_usb_a()
# Usage
legacy_stick = OldMemoryStick()
adapter = USBAdapter(legacy_stick)
# Our app thinks it's talking to a ModernUSB device
print(adapter.connect_usb_c())
Key Concept
The Adapter is often called a Wrapper. It provides a different interface to its subject.
2. The Bridge Pattern
The Problem
If you have a Shape class with subclasses Circle and Square, and you want to add colors (Red and Blue), you might end up with RedCircle, BlueCircle, RedSquare, and BlueSquare. Adding a NEW shape or a NEW color results in a “Cartesian product” explosion of classes.
The Solution
The Bridge pattern suggests that you extract one of the dimensions (e.g., Color) into a separate class hierarchy. Now the original class (Shape) has a reference to an object of the new hierarchy. This reference acts as a “bridge” between the Abstraction and the Implementation.
Java Example
// Implementation Interface
interface Device {
void setVolume(int percent);
}
// Concrete Implementations
class TV implements Device {
public void setVolume(int p) { System.out.println("TV volume: " + p); }
}
class Radio implements Device {
public void setVolume(int p) { System.out.println("Radio volume: " + p); }
}
// Abstraction
abstract class RemoteControl {
protected Device device;
public RemoteControl(Device device) { this.device = device; }
public abstract void volumeUp();
}
// Refined Abstraction
class AdvancedRemote extends RemoteControl {
private int volume = 10;
public AdvancedRemote(Device device) { super(device); }
public void volumeUp() {
volume += 10;
device.setVolume(volume);
}
}
// Usage
RemoteControl remote = new AdvancedRemote(new TV());
remote.volumeUp(); // Bridges the remote command to the TV implementation
3. Adapter vs. Bridge
While they look similar, they have different intents:
- Adapter is used to make existing classes work together. It’s usually applied after the system is designed to resolve incompatibilities.
- Bridge is used up-front in the design phase to let abstractions and implementations vary independently. It’s about decoupling.
Use Cases
- Use Adapter when you need to use a 3rd party library whose interface doesn’t match your code.
- Use Bridge when you want to avoid a permanent binding between an abstraction and its implementation (e.g., when you switch between cross-platform UI toolkits).
Summary Table
| Pattern | Goal | Relationship |
|---|---|---|
| Adapter | Interface Compatibility | Wrapper around one object |
| Bridge | Decoupling | Link between two hierarchies |
By using these patterns, you can create systems that are easier to maintain because changes in one part of the system (like the implementation detail of a TV) don’t force changes in another part (the remote control logic).