6. Adapter
The Adapter pattern converts the interface of a class into another interface that clients expect. It lets classes work together that could not otherwise because of incompatible interfaces. Think of it as a power plug adapter when traveling abroad.
Intent and Motivation
Intent: Convert the interface of a class into another interface clients expect. Adapter lets classes work together that could not otherwise because of incompatible interfaces.
Motivation: You integrate a third-party payment library whose API uses processPayment(amount, currencyCode) but your application expects charge(Money). Rewriting the library is impossible; changing every client is expensive. An Adapter wraps the library, implements your PaymentGateway interface, and translates calls internally. The client never knows the adaptee exists.
Adapters are everywhere in integration work — legacy systems, SDK wrappers, and API versioning.
Structure (UML-like)
┌──────────┐ uses ┌──────────────┐
│ Client │ ────────────────────► │ Target │ (expected interface)
└──────────┘ ├──────────────┤
│ + request() │
└──────▲───────┘
│ implements
┌──────┴───────┐
│ Adapter │
├──────────────┤
│ - adaptee │
│ + request() │ ── translates ──► ┌──────────┐
└──────────────┘ │ Adaptee │
├──────────┤
│+specific │
│ Request()│
└──────────┘
Two variants:
- Object Adapter — adapter holds a reference to the adaptee (composition; preferred).
- Class Adapter — adapter inherits both target and adaptee (multiple inheritance; Java cannot do this).
Java Example
// Target — what the client expects
interface MediaPlayer {
void play(String filename);
}
// Adaptee — existing incompatible class
class AdvancedMediaPlayer {
void playVlc(String file) {
System.out.println("Playing VLC: " + file);
}
void playMp4(String file) {
System.out.println("Playing MP4: " + file);
}
}
// Object Adapter
class MediaAdapter implements MediaPlayer {
private final AdvancedMediaPlayer advancedPlayer = new AdvancedMediaPlayer();
public void play(String filename) {
if (filename.endsWith(".vlc")) {
advancedPlayer.playVlc(filename);
} else if (filename.endsWith(".mp4")) {
advancedPlayer.playMp4(filename);
} else {
throw new IllegalArgumentException("Unsupported: " + filename);
}
}
}
// Client
class AudioPlayer implements MediaPlayer {
private final MediaAdapter adapter = new MediaAdapter();
public void play(String filename) {
if (filename.endsWith(".mp3")) {
System.out.println("Playing MP3: " + filename);
} else {
adapter.play(filename);
}
}
}
JavaScript Example
// Legacy API (adaptee)
const legacyLogger = {
log_message(level, msg) {
console.log(`[${level.toUpperCase()}] ${msg}`);
}
};
// Modern interface (target)
class Logger {
info(message) { throw new Error('Not implemented'); }
error(message) { throw new Error('Not implemented'); }
}
// Adapter
class LegacyLoggerAdapter extends Logger {
constructor(legacy) {
super();
this.legacy = legacy;
}
info(message) { this.legacy.log_message('info', message); }
error(message) { this.legacy.log_message('error', message); }
}
// Client code uses the modern interface
function processData(logger) {
logger.info('Processing started');
logger.error('Something failed');
}
processData(new LegacyLoggerAdapter(legacyLogger));
Real-World Use Cases
| Framework / System | Usage |
|---|---|
Java Collections.enumeration() |
Adapts modern Iterator to legacy Enumeration interface. |
| Java InputStreamReader | Adapts byte streams (InputStream) to character streams (Reader). |
Spring HandlerAdapter |
Adapts different controller types to a unified request handling interface. |
| Axios interceptors | Wrap and transform request/response formats between client and server APIs. |
| React refs / HOCs | Adapt class components or legacy APIs to modern functional component patterns. |
| Database ORMs | Adapt vendor-specific JDBC drivers to a unified repository interface. |
Pros and Cons
| Pros | Cons |
|---|---|
| Single Responsibility — interface conversion is isolated | Increases total number of classes/wrappers |
| Open/Closed — integrate new adaptees without changing clients | Added indirection can complicate debugging |
| Reuses existing code without modification | Adapter may become a maintenance burden if adaptee API changes frequently |
| Works with legacy and third-party code | Over-adapting can hide important adaptee behavior or errors |
| Enables gradual migration between API versions | Performance overhead from translation layer (usually negligible) |
When to Use vs When NOT to Use
Use when:
- You want to use an existing class but its interface does not match what you need.
- You need to create a reusable class that cooperates with unrelated classes.
- You are integrating third-party or legacy libraries you cannot modify.
- You are migrating to a new API while supporting old clients during transition.
Do NOT use when:
- You control both interfaces and can change them to be compatible directly.
- The adaptation logic is so complex it reimplements the entire adaptee (rewrite instead).
- You need to add behavior dynamically in layers (consider Decorator).
- You want to simplify a complex subsystem (consider Facade).
Common Mistakes
- Adapter vs Decorator confusion — Adapter changes interface; Decorator adds behavior while keeping the same interface.
- Leaking adaptee types — adapter methods should not expose the wrapped object to clients.
- Fat adapter — putting business logic in the adapter instead of pure translation.
- Not handling all adaptee error modes — map exceptions and error codes to the target interface contract.
- Creating adapters when direct refactoring is feasible — unnecessary wrappers add complexity.
Related Patterns
- Bridge — designed upfront to separate abstraction from implementation; Adapter retrofits incompatibility.
- Decorator — same interface, adds responsibilities; Adapter changes interface.
- Facade — simplifies a complex subsystem; Adapter translates one interface to another.
- Proxy — same interface, controls access; Adapter changes interface.
- Facade + Adapter — often combined when integrating legacy systems behind a simplified API.