Adapter Pattern
Convert the interface of a class into another interface clients expect. A structural design pattern for interface compatibility.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Adapter Pattern
Overview
The Adapter Pattern is a structural design pattern that allows objects with incompatible interfaces to collaborate. It wraps an existing class with a new interface so that it becomes compatible with the client’s expectations.
It is the software equivalent of a physical power adapter: it converts one interface into another without modifying the original device.
When to Use
Use the Adapter Pattern when:
- You want to use an existing class whose interface is incompatible with the rest of your code. See Strategy Pattern for runtime behavior selection.
- You need to reuse legacy or third-party code that does not match your interfaces. See Facade Pattern for simplifying complex APIs.
- You want to create a unified interface across several classes with different APIs
- You cannot or should not modify the source code of the incompatible class. See Decorator Pattern for extending behavior without subclassing.
- You need to translate data formats or calling conventions between systems
Solution
Python
class OldPrinter:
def old_print(self, text: str):
print(f"OldPrinter: {text}")
class PrinterAdapter:
def __init__(self, old_printer: OldPrinter):
self._old = old_printer
def print(self, text: str):
self._old.old_print(text)
# Usage
adapter = PrinterAdapter(OldPrinter())
adapter.print("Hello World") # OldPrinter: Hello World
JavaScript
class OldPrinter {
oldPrint(text) {
console.log(`OldPrinter: ${text}`);
}
}
class PrinterAdapter {
constructor(oldPrinter) {
this.old = oldPrinter;
}
print(text) {
this.old.oldPrint(text);
}
}
// Usage
const adapter = new PrinterAdapter(new OldPrinter());
adapter.print("Hello World"); // OldPrinter: Hello World
Java
class OldPrinter {
void oldPrint(String text) {
System.out.println("OldPrinter: " + text);
}
}
interface ModernPrinter {
void print(String text);
}
class PrinterAdapter implements ModernPrinter {
private final OldPrinter oldPrinter;
PrinterAdapter(OldPrinter oldPrinter) {
this.oldPrinter = oldPrinter;
}
public void print(String text) {
oldPrinter.oldPrint(text);
}
}
// Usage
ModernPrinter printer = new PrinterAdapter(new OldPrinter());
printer.print("Hello World"); // OldPrinter: Hello World
Explanation
The Adapter Pattern consists of:
- Target Interface (
ModernPrinter): The interface the client expects - Adaptee (
OldPrinter): The existing class with the incompatible interface - Adapter (
PrinterAdapter): Wraps the adaptee and exposes the target interface
The adapter translates calls from the target interface into calls the adaptee understands. Neither the client nor the adaptee needs to change.
Variants
| Variant | Use Case | Trade-off |
|---|---|---|
| Object Adapter | Wraps an instance (composition) | Flexible, can adapt subclasses |
| Class Adapter | Inherits from adaptee (multiple inheritance) | Less flexible, not possible in all languages |
| Two-way Adapter | Both interfaces are usable | More complex, but bidirectional |
What Works
- Favor composition over inheritance for adapters (object adapter pattern)
- Keep the adapter thin: It should translate calls, not add business logic
- Document the mapping: Explain how target methods map to adaptee methods
- Handle nulls and exceptions gracefully during translation
- Consider caching: If translation involves heavy computation, cache results
Advanced Techniques
Two-way adapter for bidirectional compatibility
Support both target and adaptee interfaces for maximum flexibility:
# Python: Two-way adapter
class OldPrinter:
def old_print(self, text: str):
print(f"OldPrinter: {text}")
class ModernPrinter:
def print(self, text: str):
print(f"ModernPrinter: {text}")
class BiDirectionalPrinterAdapter:
def __init__(self, old_printer: OldPrinter, new_printer: ModernPrinter):
self._old = old_printer
self._new = new_printer
def print(self, text: str):
# Target interface (ModernPrinter)
self._old.old_print(text)
def old_print(self, text: str):
# Adaptee interface (OldPrinter)
self._new.print(text)
# Usage with both interfaces
adapter = BiDirectionalPrinterAdapter(OldPrinter(), ModernPrinter())
adapter.print("Hello") # Uses ModernPrinter interface, calls OldPrinter
adapter.old_print("World") # Uses OldPrinter interface, calls ModernPrinter
Adapter with data transformation
Transform data formats between incompatible representations:
// Java: Data transformation adapter
class LegacyData {
String[] names;
int[] ages;
}
class ModernPerson {
String name;
int age;
}
interface PersonRepository {
List<ModernPerson> getAllPeople();
}
class LegacyDataAdapter implements PersonRepository {
private final LegacyData legacyData;
LegacyDataAdapter(LegacyData legacyData) {
this.legacyData = legacyData;
}
public List<ModernPerson> getAllPeople() {
List<ModernPerson> people = new ArrayList<>();
for (int i = 0; i < legacyData.names.length; i++) {
ModernPerson person = new ModernPerson();
person.name = legacyData.names[i];
person.age = legacyData.ages[i];
people.add(person);
}
return people;
}
}
Adapter with caching for performance
Add caching to expensive translation operations:
// JavaScript: Cached adapter
class CachedAdapter {
constructor(adaptee) {
this.adaptee = adaptee;
this.cache = new Map();
}
async getData(key) {
if (this.cache.has(key)) {
return this.cache.get(key);
}
const data = await this.adaptee.fetchLegacyData(key);
const transformed = this.transform(data);
this.cache.set(key, transformed);
return transformed;
}
transform(data) {
// Expensive transformation logic
return data.map(item => ({
id: item.legacyId,
name: item.legacyName,
value: item.legacyValue * 2
}));
}
invalidate(key) {
this.cache.delete(key);
}
}
Adapter composition for multiple adaptees
Wrap multiple adaptees to provide a unified interface:
# Python: Multi-adaptee adapter
class DatabaseReader:
def read_user(self, user_id: int) -> dict:
return {"id": user_id, "name": "DB User"}
class CacheReader:
def get_user(self, user_id: int) -> dict:
return {"id": user_id, "name": "Cached User"}
class UnifiedUserAdapter:
def __init__(self, db_reader: DatabaseReader, cache_reader: CacheReader):
self.db = db_reader
self.cache = cache_reader
def get_user(self, user_id: int) -> dict:
# Try cache first, fall back to database
try:
return self.cache.get_user(user_id)
except KeyError:
return self.db.read_user(user_id)
# Usage
adapter = UnifiedUserAdapter(DatabaseReader(), CacheReader())
user = adapter.get_user(123)
Adapter with retry logic
Add retry logic for unreliable adaptees:
// Java: Adapter with retry
class RetryAdapter implements ModernPrinter {
private final OldPrinter adaptee;
private final int maxRetries;
private final long retryDelayMs;
RetryAdapter(OldPrinter adaptee, int maxRetries, long retryDelayMs) {
this.adaptee = adaptee;
this.maxRetries = maxRetries;
this.retryDelayMs = retryDelayMs;
}
public void print(String text) {
int attempts = 0;
while (attempts <= maxRetries) {
try {
adaptee.oldPrint(text);
return;
} catch (Exception e) {
attempts++;
if (attempts <= maxRetries) {
try {
Thread.sleep(retryDelayMs);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted during retry", ie);
}
}
}
}
throw new RuntimeException("Failed after " + maxRetries + " retries");
}
}
Adapter with logging and monitoring
Add observability to adapter operations:
// JavaScript: Adapter with logging
class LoggingAdapter {
constructor(adaptee, logger) {
this.adaptee = adaptee;
this.logger = logger;
}
print(text) {
const startTime = Date.now();
this.logger.info('Adapter: Calling adaptee with text', { text });
try {
this.adaptee.oldPrint(text);
const duration = Date.now() - startTime;
this.logger.info('Adapter: Call succeeded', { duration });
} catch (error) {
const duration = Date.now() - startTime;
this.logger.error('Adapter: Call failed', { error: error.message, duration });
throw error;
}
}
}
Best Practices
- Favor composition over inheritance. Use object adapters (composition) rather than class adapters (inheritance) for greater flexibility.
- Keep adapters thin. Adapters should only translate interfaces, not add business logic or complex behavior.
- Document the mapping. Clearly document how target methods map to adaptee methods to aid future maintenance.
- Handle exceptions gracefully. Translate adaptee exceptions into appropriate target exceptions or handle them appropriately.
- Consider caching. If translation involves expensive computation, cache results to improve performance.
- Use interfaces for targets. Define clear target interfaces to make adapters interchangeable and testable.
- Avoid cascading adapters. Chaining multiple adapters creates indirection and makes debugging difficult.
- Test adapters thoroughly. Write unit tests for the translation logic and integration tests with real adaptees.
- Monitor adapter performance. Track metrics on adapter call latency, error rates, and cache hit rates.
- Version adapters when needed. If the adaptee interface changes, version your adapters to support multiple versions simultaneously.
Common Mistakes
- Fat adapters. Adding business logic instead of just interface translation. Keep adapters focused on translation only.
- Leaky adapters. Exposing adaptee methods through the adapter interface, breaking the abstraction.
- Cascading adapters. Chaining multiple adapters creates indirection hell and makes maintenance difficult.
- Ignoring exceptions. Not translating or handling adaptee errors properly, leading to inconsistent error handling.
- Modifying the adaptee. The whole point of the adapter pattern is to leave the original class untouched.
- Over-engineering simple cases. Using adapters when a simple wrapper function would suffice.
- Tight coupling to adaptee. Making the adapter too dependent on specific implementation details of the adaptee.
- Forgetting null checks. Not handling null or undefined values from the adaptee, causing runtime errors.
- Skipping documentation. Failing to document the translation logic makes future maintenance difficult.
- Mixing concerns. Combining adapter logic with other concerns like logging, caching, or retry logic in a single class.
Frequently Asked Questions
Q: What is the difference between Adapter and Facade? A: Adapter makes one incompatible interface compatible. Facade simplifies a complex subsystem by providing a single unified interface to multiple classes.
Q: Can I adapt multiple classes at once? A: Yes. A single adapter can wrap multiple adaptees and coordinate them to provide a unified interface.
Q: Is Adapter a workaround for bad design? A: Sometimes, but often it is a pragmatic bridge when integrating external or legacy code that you cannot modify.
Q: How does Adapter differ from Decorator? A: Adapter changes the interface of an object. Decorator adds behavior without changing the interface.
Q: Should I use Adapter or Strategy for runtime behavior selection? A: Use Strategy when you need to swap algorithms at runtime. Use Adapter when you need to make incompatible interfaces work together.
Q: Can adapters be nested? A: While technically possible, nesting adapters (cascading) is generally discouraged as it creates indirection and makes code difficult to understand and debug.
Q: How do I test an adapter? A: Write unit tests with mock adaptees to test the translation logic. Write integration tests with real adaptees to verify end-to-end behavior.
Q: Should adapters handle authentication? A: No. Authentication should be handled separately, typically by an HTTP client or interceptor. Adapters should focus only on interface translation.
Q: Can I use Adapter for data format conversion? A: Yes. Adapters are commonly used to transform data formats between different representations (e.g., XML to JSON, legacy formats to modern formats).
Q: Is this pattern suitable for small projects? A: For small projects with few components, this pattern may add unnecessary complexity. Start simple and introduce the pattern when you feel the pain it solves.
Q: How does this pattern compare to alternatives? A: Each pattern makes different trade-offs. Review the variants table above and consider your specific constraints: team size, performance requirements, and future scaling plans.
Q: Can I partially apply this pattern? A: Yes. Many teams adopt patterns incrementally. Start with the core idea and add sophistication as needed. The pattern is a guide, not a strict blueprint.
Related Resources
Decorator Pattern
Add new functionality to objects dynamically by wrapping them. A structural design pattern for flexible behavior extension.
PatternCommand Pattern
Encapsulate a request as an object, letting you parameterize clients with queues, logs, and undoable operations. A behavioral design pattern.
RecipeCall a REST API
How to make HTTP requests to a REST API and handle the JSON response in multiple languages.
PatternBridge Pattern
Decouple an abstraction from its implementation so both can vary independently. A structural design pattern for platform independence.
PatternDecorator Pattern for HTTP Request Pipelines
Use the Decorator pattern to compose cross-cutting concerns like logging, metrics, and retries into HTTP request pipelines without modifying core logic
PatternFacade Pattern
Provide a simplified interface to a complex subsystem. A structural pattern that hides implementation details behind a clean API.
PatternProxy Pattern for API Response Caching
How to implement a caching proxy that intercepts API calls and stores responses to reduce latency and avoid redundant network requests