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.
Los ves por todas partes en sistemas reales, aunque nadie los llame así. Un SDK de pagos envuelto detrás de tu propia interfaz PaymentGateway, un repositorio ORM que traduce objetos de dominio a filas de base de datos, un servicio de notificaciones que normaliza email, SMS y push detrás de un único send(recipient, message) — todos son adapters. El patrón se gana su sitio cuando una frontera que no controlas se encuentra con una interfaz que sí controlas.
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
Sáltate el patrón cuando el código incompatible es tuyo — simplemente cámbialo. Un adapter es el precio que pagas por una frontera que no puedes modificar: un SDK de un proveedor, un módulo legacy, un cliente generado. Introducir uno entre dos clases que escribe tu propio equipo suele añadir indirección sin comprar nada.
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.
El cliente trabaja contra Target, el adapter mantiene una referencia a OldPrinter y reenvía las llamadas traducidas, y OldPrinter queda intacto. Esa indirección es todo el mecanismo — y también su coste. Cada adapter añade un tipo más que nombrar, probar y recorrer al depurar, así que el patrón compensa cuando la alternativa es peor: reescribir el adaptee, hacer un fork de la librería del proveedor, o dejar que objetos con forma legacy se filtren a tu modelo de dominio.
Variantes
El object adapter — composición — es la opción por defecto: funciona en cualquier lenguaje y puede envolver una jerarquía de clases completa. El class adapter solo existe en lenguajes con herencia múltiple (C++, Python) y te acopla al tipo concreto del adaptee. El two-way adapter aparece sobre todo en capas de frontera que deben satisfacer dos contratos existentes a la vez.
| 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 |
Adapter vs Patrones Relacionados
Adapter se confunde fácilmente con las otras envolturas estructurales. La diferencia está en la intención:
| Patrón | Qué cambia | Pregunta típica que responde |
|---|---|---|
| Adapter | La interfaz, para que dos tipos incompatibles trabajen juntos | ”¿Cómo llamo a esta clase legacy o del proveedor?” |
| Facade | Nada — simplifica una API compleja existente | ”¿Cómo oculto la complejidad de este subsistema?” |
| Decorator | El comportamiento — añade responsabilidades, misma interfaz | ”¿Cómo lo extiendo sin heredar?” |
| Proxy | El acceso — controla cómo se alcanza el objeto real | ”¿Cómo controlo o difiero este objeto?” |
Si las interfaces ya coinciden y solo quieres un punto de entrada más simple, lo que buscas es Facade. Si el objetivo es añadir comportamiento, Decorator. Adapter solo trata de hacer coincidir firmas que no encajan.
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 traducen a métodos del adaptee
- Maneja nulos y excepciones de forma controlada durante la traducción
- Considera caching: Si la traducción implica cálculo pesado, guarda los resultados en caché
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 caché primero, recurre a la base de datos si falla
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 reintento
Añade lógica de reintento para adaptees no fiables:
// 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 + " reintentos");
}
}
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;
}
}
}
Probar un Adapter
Probar un adapter es sobre todo probar el mapeo. Simula el adaptee, llama a cada método target, y verifica que el adaptee recibió los argumentos correctos y que el cliente obtuvo el resultado traducido esperado. Los fallos que vale la pena cubrir no son el camino feliz — son los bordes de la traducción: nulos que el adaptee no espera, excepciones con tipos propios del adaptee, y unidades o enumerados que no coinciden entre las dos interfaces. Si el adapter añade caché o reintentos, prueba esos comportamientos por separado para que un acierto de caché no enmascare un adaptee roto en tus tests.
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 de forma controlada. Traduce excepciones del adaptee a excepciones target apropiadas o manéjalas adecuadamente.
- Considera caché. Si la traducción implica cálculo costoso, guarda resultados en caché para mejorar el rendimiento.
- Usa interfaces para targets. Define interfaces target claras para hacer los adapters intercambiables y fáciles de probar.
- Evita adapters en cascada. Encadenar múltiples adapters crea indirección y dificulta la depuración.
- Prueba adapters exhaustivamente. Escribe tests unitarios para la lógica de traducción y tests de integración con adaptees reales.
- Monitorea el rendimiento del adapter. Rastrea métricas de latencia de llamadas, tasas de error y aciertos de caché.
- 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 dificulta el mantenimiento.
- Ignorar excepciones. No traducir o manejar errores del adaptee adecuadamente, lo que lleva a un 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 envolvente simple sería suficiente.
- Acoplamiento fuerte al adaptee. Hacer el adapter demasiado dependiente de detalles de implementación específicos del adaptee.
- Olvidar comprobaciones de null. No manejar valores null o undefined del adaptee, causando errores en ejecución.
- Saltar la documentación. No documentar la lógica de traducción dificulta el mantenimiento futuro.
- Mezclar responsabilidades. Combinar lógica de adapter con otras responsabilidades como logging, caché o reintentos en una sola clase.
See Also
- Adapter Pattern para Integrar APIs REST Externas — el mismo patrón especializado en normalizar respuestas de APIs de terceros
- Une Interfaces Incompatibles con el Patrón Adapter — un recorrido más corto, orientado al código
- Repositorio companion — los ejemplos del adapter como archivos ejecutables en Python, JavaScript y Java
- Adapter — Refactoring.Guru — referencia canónica con más ejemplos y diagramas
- Design Patterns (GoF) — el catálogo original del que viene el patrón
Preguntas frecuentes
¿Cuál es la diferencia entre Adapter y Facade?
Adapter hace compatible una interfaz incompatible. Facade simplifica un subsistema complejo proporcionando una única interfaz unificada a múltiples clases.
¿Puedo adaptar múltiples clases a la vez?
Sí — un único adapter puede envolver varios adaptees y coordinarlos detrás de una interfaz unificada, como en el ejemplo multi-adaptee de arriba.
¿Es Adapter una solución provisional para mal diseño?
A veces — pero más a menudo es un puente pragmático para integrar código externo o legacy que no puedes cambiar.
¿Cómo se diferencia Adapter de Decorator?
Adapter reescribe la interfaz de un objeto; Decorator mantiene la interfaz y le apila comportamiento.
¿Debería usar Adapter o Strategy para selección de comportamiento en runtime?
Usa Strategy para intercambiar algoritmos en tiempo de ejecución. Acude a Adapter cuando el problema son firmas que no encajan.
¿Pueden los adapters anidarse?
Es posible, pero un adapter llamando a otro adapter acumula indirección rápido — tómalo como un olor a mal diseño y aplana la cadena.
¿Cómo pruebo un adapter?
Simula el adaptee y verifica que cada método target se mapea correctamente; la sección Probar un Adapter de arriba recorre los casos borde que vale la pena cubrir.
¿Deberían los adapters manejar autenticación?
No — mantén la autenticación en el cliente HTTP o en un interceptor. Los adapters traducen interfaces, nada más.
¿Puedo usar Adapter para conversión de formato de datos?
Sí — convertir XML a JSON o un esquema legacy a uno moderno es un trabajo clásico de adapter.
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 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.
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.
PatternPatrón Proxy
Proporciona un sustituto o marcador de posición para otro objeto para controlar el acceso a él. Un patrón estructural para control de acceso, carga perezosa y logging.
PatternAdapter Pattern para Integrar APIs REST Externas
Usa el Adapter pattern para normalizar respuestas de APIs REST externas en un modelo interno consistente sin filtrar formatos de terceros en tu dominio
RecipePuente entre Interfaces Incompatibles con el Adapter Pattern
Integra APIs legacy y librerías de terceros con object, class y two-way adapters en Java, TypeScript y Python.