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.
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.
Recursos Relacionados
Patrón Decorator
Añade nueva funcionalidad a objetos dinámicamente envolviéndolos. Patrón de diseño estructural para extensión flexible de comportamiento.
PatternPatrón Command
Encapsula una petición como un objeto, permitiendo parametrizar clientes con colas, logs y operaciones deshacibles. Patrón de diseño conductual.
RecipeLlamar a una API REST: Python, JS, Java y Go
Cómo hacer peticiones HTTP a una API REST y manejar la respuesta JSON en Python, JavaScript, Java y Go.
PatternPatrón Bridge: Desacopla Abstracción e Implementación
Dividí una clase en dos jerarquías — abstracción e implementación — para que ambas evolucionen independientemente. Con ejemplos en Python, Java y JavaScript.
PatternDecorator Pattern para Pipelines de Peticiones HTTP
Usa el Decorator pattern para componer preocupaciones transversales como logging, metricas y reintentos en pipelines de peticiones HTTP sin modificar logica central
PatternPatrón Facade
Provee una interfaz simplificada a un subsistema complejo. Un patrón estructural que oculta detalles de implementación detrás de una API limpia.