Patrón Adapter
Convierte la interfaz de una clase en otra interfaz que los clientes esperan. Patrón de diseño estructural para compatibilidad de interfaces.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Patrón Adapter
Visión general
El Patrón Adapter es un patrón de diseño estructural que permite que objetos con interfaces incompatibles colaboren. Envuelve una clase existente con una nueva interfaz para que sea compatible con las expectativas del cliente.
Es el equivalente software de un adaptador de corriente físico: convierte una interfaz en otra sin modificar el dispositivo original.
Cuándo usarlo
Usa el Patrón Adapter cuando:
- Quieres usar una clase existente cuya interfaz es incompatible con el resto de tu código. Consulta Strategy Pattern para selección de comportamiento en runtime.
- Necesitas reutilizar código legacy o de terceros que no coincide con tus interfaces. Consulta Facade Pattern para simplificar APIs complejas.
- Quieres crear una interfaz unificada a través de varias clases con APIs diferentes
- No puedes o no deberías modificar el código fuente de la clase incompatible. Consulta Decorator Pattern para extender comportamiento sin herencia.
- Necesitas traducir formatos de datos o convenciones de llamada entre sistemas
Solución
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)
# Uso
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);
}
}
// Uso
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);
}
}
// Uso
ModernPrinter printer = new PrinterAdapter(new OldPrinter());
printer.print("Hello World"); // OldPrinter: Hello World
Explicación
El Patrón Adapter consiste en:
- Interfaz Target (
ModernPrinter): La interfaz que el cliente espera - Adaptee (
OldPrinter): La clase existente con la interfaz incompatible - Adapter (
PrinterAdapter): Envuelve el adaptee y expone la interfaz target
El adapter traduce llamadas desde la interfaz target a llamadas que el adaptee entiende. Ni el cliente ni el adaptee necesitan cambiar.
Variantes
| Variante | Caso de uso | Compromiso |
|---|---|---|
| Object Adapter | Envuelve una instancia (composición) | Flexible, puede adaptar subclases |
| Class Adapter | Hereda del adaptee (herencia múltiple) | Menos flexible, no posible en todos los lenguajes |
| Two-way Adapter | Ambas interfaces son usables | Más complejo, pero bidireccional |
Lo que funciona
- Prefiere composición sobre herencia para adapters (object adapter pattern)
- Mantén el adapter delgado: Debería traducir llamadas, no añadir lógica de negocio
- Documenta el mapeo: Explica cómo los métodos target se mapean a métodos del adaptee
- Maneja nulls y excepciones gracefulmente durante la traducción
- Considera caching: Si la traducción involucra computación pesada, cachea resultados
Técnicas Avanzadas
Adapter bidireccional para compatibilidad bidireccional
Soporta ambas interfaces target y adaptee para máxima flexibilidad:
# Python: Adapter bidireccional
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):
# Interfaz target (ModernPrinter)
self._old.old_print(text)
def old_print(self, text: str):
# Interfaz adaptee (OldPrinter)
self._new.print(text)
# Uso con ambas interfaces
adapter = BiDirectionalPrinterAdapter(OldPrinter(), ModernPrinter())
adapter.print("Hello") # Usa interfaz ModernPrinter, llama OldPrinter
adapter.old_print("World") # Usa interfaz OldPrinter, llama ModernPrinter
Adapter con transformación de datos
Transforma formatos de datos entre representaciones incompatibles:
// Java: Adapter de transformación de datos
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 con cache para rendimiento
Añade cache a operaciones de traducción costosas:
// JavaScript: Adapter con cache
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) {
// Lógica de transformación costosa
return data.map(item => ({
id: item.legacyId,
name: item.legacyName,
value: item.legacyValue * 2
}));
}
invalidate(key) {
this.cache.delete(key);
}
}
Composición de adapter para múltiples adaptees
Envuelve múltiples adaptees para proporcionar una interfaz unificada:
# Python: Adapter multi-adaptee
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:
# Intenta cache primero, fallback a base de datos
try:
return self.cache.get_user(user_id)
except KeyError:
return self.db.read_user(user_id)
# Uso
adapter = UnifiedUserAdapter(DatabaseReader(), CacheReader())
user = adapter.get_user(123)
Adapter con lógica de retry
Añade lógica de retry para adaptees no confiables:
// Java: Adapter con 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("Interrumpido durante retry", ie);
}
}
}
}
throw new RuntimeException("Falló después de " + maxRetries + " retries");
}
}
Adapter con logging y monitoreo
Añade observabilidad a operaciones de adapter:
// JavaScript: Adapter con logging
class LoggingAdapter {
constructor(adaptee, logger) {
this.adaptee = adaptee;
this.logger = logger;
}
print(text) {
const startTime = Date.now();
this.logger.info('Adapter: Llamando adaptee con texto', { text });
try {
this.adaptee.oldPrint(text);
const duration = Date.now() - startTime;
this.logger.info('Adapter: Llamada exitosa', { duration });
} catch (error) {
const duration = Date.now() - startTime;
this.logger.error('Adapter: Llamada fallida', { error: error.message, duration });
throw error;
}
}
}
Mejores Prácticas
- Prefiere composición sobre herencia. Usa adapters de objeto (composición) en lugar de adapters de clase (herencia) para mayor flexibilidad.
- Mantén los adapters delgados. Los adapters deberían solo traducir interfaces, no añadir lógica de negocio o comportamiento complejo.
- Documenta el mapeo. Documenta claramente cómo los métodos target se mapean a métodos del adaptee para ayudar al mantenimiento futuro.
- Maneja excepciones gracefulmente. Traduce excepciones del adaptee a excepciones target apropiadas o manéjalas apropiadamente.
- Considera cache. Si la traducción involucra computación costosa, cachea resultados para mejorar rendimiento.
- Usa interfaces para targets. Define interfaces target claras para hacer los adapters intercambiables y testeables.
- Evita adapters en cascada. Encadenar múltiples adapters crea indirección y hace el debugging difícil.
- Prueba adapters exhaustivamente. Escribe unit tests para la lógica de traducción e integration tests con adaptees reales.
- Monitorea rendimiento de adapter. Rastrea métricas de latencia de llamadas de adapter, tasas de error y tasas de cache hit.
- Versiona adapters cuando sea necesario. Si la interfaz del adaptee cambia, versiona tus adapters para soportar múltiples versiones simultáneamente.
Errores Comunes
- Adapters gordos. Añadir lógica de negocio en lugar de solo traducción de interfaz. Mantén los adapters enfocados solo en traducción.
- Adapters filtrados. Exponer métodos del adaptee a través de la interfaz del adapter, rompiendo la abstracción.
- Adapters en cascada. Encadenar múltiples adapters crea un infierno de indirección y hace el mantenimiento difícil.
- Ignorar excepciones. No traducir o manejar errores del adaptee apropiadamente, llevando a manejo de errores inconsistente.
- Modificar el adaptee. Todo el punto del patrón adapter es dejar la clase original intacta.
- Sobre-ingeniería de casos simples. Usar adapters cuando una función wrapper simple sería suficiente.
- Acoplamiento tight al adaptee. Hacer el adapter demasiado dependiente de detalles de implementación específicos del adaptee.
- Olvidar checks de null. No manejar valores null o undefined del adaptee, causando errores en runtime.
- Saltar documentación. Fallar en documentar la lógica de traducción hace el mantenimiento futuro difícil.
- Mezclar concerns. Combinar lógica de adapter con otros concerns como logging, cache o lógica de retry en una sola clase.
Preguntas frecuentes
P: ¿Cuál es la diferencia entre Adapter y Facade? R: Adapter hace compatible una interfaz incompatible. Facade simplifica un subsistema complejo proporcionando una única interfaz unificada a múltiples clases.
P: ¿Puedo adaptar múltiples clases a la vez? R: Sí. Un único adapter puede envolver múltiples adaptees y coordinarlos para proporcionar una interfaz unificada.
P: ¿Es Adapter un workaround para mal diseño? R: A veces, pero a menudo es un puente pragmático cuando integras código externo o legacy que no puedes modificar.
P: ¿Cómo se diferencia Adapter de Decorator? R: Adapter cambia la interfaz de un objeto. Decorator añade comportamiento sin cambiar la interfaz.
P: ¿Debería usar Adapter o Strategy para selección de comportamiento en runtime? R: Usa Strategy cuando necesitas intercambiar algoritmos en runtime. Usa Adapter cuando necesitas hacer que interfaces incompatibles trabajen juntas.
P: ¿Pueden los adapters anidarse? R: Aunque técnicamente posible, anidar adapters (cascada) generalmente se desaconseja ya que crea indirección y hace el código difícil de entender y debuggear.
P: ¿Cómo pruebo un adapter? R: Escribe unit tests con adaptees mock para probar la lógica de traducción. Escribe integration tests con adaptees reales para verificar comportamiento end-to-end.
P: ¿Deberían los adapters manejar autenticación? R: No. La autenticación debería manejarse separadamente, típicamente por un cliente HTTP o interceptor. Los adapters deberían enfocarse solo en traducción de interfaz.
P: ¿Puedo usar Adapter para conversión de formato de datos? R: Sí. Los adapters se usan comúnmente para transformar formatos de datos entre diferentes representaciones (ej. XML a JSON, formatos legacy a formatos modernos).
P: ¿Es este patrón adecuado para proyectos pequeños? R: Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.
P: ¿Cómo se compara este patrón con alternativas? R: Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.
P: ¿Puedo aplicar este patrón parcialmente? R: Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.
Recursos Relacionados
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