🎯 Patrón Adapter — Cheatsheet Completo 🎯
El patrón Adapter es un patrón estructural que permite la cooperación entre objetos con interfaces incompatibles, actuando como un traductor o puente que convierte las llamadas de un contrato a otro. Resuelve problemas de integración con sistemas legacy, librerías de terceros, APIs versionadas o componentes con semánticas divergentes, sin modificar el código fuente de ninguna de las partes. Nace para preservar la inversión en código existente, unificar múltiples proveedores bajo una interfaz homogénea y aislar la lógica de negocio de los detalles de integración externa. Este cheatsheet cubre la intención arquitectónica real, contratos de traducción seguros, implementaciones multi-paradigma, variantes de composición y delegación, impacto en testing y rendimiento, trampas de abstracción permeable, y criterios estrictos para decidir cuándo la adaptación es una necesidad del dominio y no una capa innecesaria que oculta incompatibilidades críticas.
1. 🌟 Conceptos Fundamentales
- Target (Objetivo): Interfaz o contrato que el cliente espera utilizar. Define los métodos, firmas y comportamientos estándar del sistema actual.
- Por qué importa: Establece el límite de abstracción. El cliente solo depende de esta interfaz, nunca de implementaciones externas.
- Adaptee (Adaptado): Clase, servicio o API existente con una interfaz incompatible o semánticamente distinta. Contiene la funcionalidad real que se necesita aprovechar.
- Por qué importa: Representa la realidad del ecosistema (legacy, vendor, protocolo). No puede ni debe modificarse directamente.
- Adapter (Adaptador): Componente que implementa
Targety contiene una referencia aAdaptee. Traduce llamadas, mapea parámetros y normaliza respuestas.- Por qué importa: Centraliza la lógica de conversión. Cambiar de proveedor o versión solo requiere sustituir el adaptador, no reescribir la aplicación.
- Cliente: Código que invoca métodos de
Targetsin conocimiento delAdapteeni del proceso de adaptación.- Por qué importa: Mantiene el principio de inversión de dependencias. La lógica de negocio permanece pura, predecible y testeable.
- Composición vs Herencia: El adaptador puede usar herencia (Class Adapter) o contener al
Adapteecomo campo (Object Adapter).- Por qué importa: La composición es preferida en la mayoría de lenguajes modernos. Permite adaptar múltiples fuentes, evita acoplamiento rígido y cumple el principio de sustitución de Liskov.
- Traducción de Semántica: No solo cambia nombres de métodos. Mapea tipos, normaliza errores, ajusta paginación, transforma zonas horarias o adapta convenciones de serialización.
- Por qué importa: La incompatibilidad rara vez es solo nominal. El adaptador debe manejar diferencias estructurales y de comportamiento para garantizar coherencia.
- Aislamiento de Cambios Externos: Cuando
Adapteeevoluciona (breaking changes, deprecaciones), solo elAdapterrequiere actualización.- Por qué importa: Contiene el impacto de migraciones. El resto del sistema sigue funcionando mientras se despliega la nueva versión del adaptador.
2. 📐 Estructura y Contrato de Traducción
La arquitectura sigue un flujo estricto de delegación y mapeo. El patrón garantiza que el cliente interactúe siempre con un contrato estable, independientemente de la complejidad del sistema externo.
Cliente
│
▼ invoca
Target (Interfaz)
+---------------------+
| request(): Response |
+---------------------+
▲
| implementa
Adapter
+---------------------+
| adaptee: Adaptee |
| request() |
| > adapta params |
| > delega a adaptee|
| > normaliza result|
+---------------------+
│ delega
▼
Adaptee (Sistema Externo)
+---------------------+
| specificCall(): Raw |
+---------------------+
Flujo de ejecución garantizado:
- Cliente invoca
target.request(data). Adapterrecibe la llamada, valida y transformadataal formato esperado porAdaptee.Adapterinvocaadaptee.specificCall(transformedData).Adapteeretorna respuesta cruda o con estructura distinta.Adapternormaliza errores, mapea campos y ajusta tipos.- Retorna
Responsecompatible conTarget. El cliente nunca ve la lógica de conversión.
Contrato mínimo en pseudocódigo tipado:
// Target: Lo que el sistema espera
interface PaymentGateway {
process(amount: number, currency: string): Promise<Transaction>;
}
// Adaptee: Librería de terceros legacy
class LegacyStripeSDK {
createCharge(opts: { cents: number; curr: string }): Promise<RawCharge> {
// SDK antiguo, usa centavos y códigos distintos
}
}
// Adapter: Traductor seguro
class StripeAdapter implements PaymentGateway {
constructor(private sdk: LegacyStripeSDK) {}
async process(amount: number, currency: string): Promise<Transaction> {
// 1. Validación y mapeo
const opts = { cents: Math.round(amount * 100), curr: currency.toUpperCase() };
// 2. Delegación
const raw = await this.sdk.createCharge(opts);
// 3. Normalización
return {
id: raw.charge_id,
status: raw.status === 'success' ? 'COMPLETED' : 'FAILED',
amount: raw.amount_cents / 100,
timestamp: new Date(raw.created_at)
};
}
}
Regla inquebrantable: El Adapter nunca debe exponer el Adaptee directamente. Toda interacción debe pasar por la traducción. Si se filtra la referencia, el patrón pierde su propósito de aislamiento.
3.
Implementación por Paradigma y Ecosistema
El patrón se adapta al modelo de tipos y al estilo de integración del entorno. No requiere necesariamente herencia clásica.
3.1. POO Clásica con Delegación (TypeScript / Java / C#)
Uso de composición explícita. Ideal para aislar SDKs, drivers o APIs REST con contratos divergentes.
public interface DatabaseDriver {
ResultSet executeQuery(String sql) throws SQLException;
}
public class PostgreSQLAdapter implements DatabaseDriver {
private final org.postgresql.ds.PGPoolingDataSource pool;
public PostgreSQLAdapter(PGPoolingDataSource pool) {
this.pool = pool;
}
@Override
public ResultSet executeQuery(String sql) throws SQLException {
// Traduce contrato genérico a implementación específica
Connection conn = pool.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql);
return stmt.executeQuery(); // Delegación controlada
}
}
// Cliente usa DatabaseDriver sin conocer PostgreSQL ni JDBC directo.
3.2. Enfoque Funcional / Closures (JavaScript / Python)
Se reemplaza herencia por funciones de orden superior o objetos inmutables que mapean entradas/salidas.
// Adaptee: API REST v1 (snake_case, status string)
const legacyApi = {
fetchUser: async (id) => ({ data: { first_name: "Ana", status: "active" } })
};
// Adapter: Traductor funcional
function createUserAdapter(api) {
return {
getUser: async (id) => {
const res = await api.fetchUser(id);
// Mapeo seguro y transformación
return {
fullName: `${res.data.first_name} ${res.data.last_name || ""}`,
isActive: res.data.status === "active",
metadata: { source: "legacy_v1" }
};
}
};
}
// Uso: const modernApi = createUserAdapter(legacyApi);
// modernApi.getUser(42).then(console.log);
Ventaja: Extensión sin boilerplate. Desventaja: Pérdida de validación estática si no se usa typescript o typing.Protocol.
3.3. Traits / Interfaces Estrictas (Rust / Go)
Uso de traits o interfaces para definir el contrato de adaptación. Garantía en tiempo de compilación.
// Target contract
pub trait NotificationService {
fn send(&self, to: &str, msg: &str) -> Result<(), String>;
}
// Adaptee: Third-party crate
pub struct SlackClient {
webhook_url: String,
}
impl SlackClient {
pub fn post_to_channel(&self, channel: &str, text: &str) -> Result<(), reqwest::Error> {
// Lógica interna del crate
Ok(())
}
}
// Adapter implementation
pub struct SlackAdapter {
client: SlackClient,
default_channel: String,
}
impl NotificationService for SlackAdapter {
fn send(&self, _to: &str, msg: &str) -> Result<(), String> {
self.client
.post_to_channel(&self.default_channel, msg)
.map_err(|e| e.to_string())
}
}
3.4. Wrapper de API / Versionado
Se usa para migrar entre versiones sin romper clientes existentes. El adaptador coexiste hasta la deprecación completa.
class APIv2Adapter implements APIv1Contract {
constructor(private v2Client: APIClient) {}
async getUsers(page: number): Promise<UserList> {
// v1 usa offset, v2 usa cursor-based pagination
const cursor = page > 1 ? this.encodeCursor(page) : undefined;
const v2Response = await this.v2Client.fetchUsers({ cursor, limit: 50 });
return {
users: v2Response.items,
total: v2Response.meta.total,
// Adapta estructura de error v2 a formato v1 esperado
error: v2Response.meta.hasError ? v2Response.meta.errors[0].message : null
};
}
}
4. 🔄 Variantes Arquitectónicas y Extensiones
| Variante | Mecanismo | Caso de uso | Trade-off |
|---|---|---|---|
| Object Adapter | Composición + delegación. Contiene referencia al Adaptee. | Integración de SDKs, APIs REST, drivers. | Ligeramente más verboso. Requiere forwarding explícito. |
| Class Adapter | Herencia múltiple o mix-in. El Adapter extiende Target y Adaptee. | Lenguajes que lo permiten (C++, Python mix-ins). | Acoplamiento rígido. Dificulta testing y evolución independiente. |
| Bidireccional | Traduce en ambas direcciones (A > B y B > A). | Sincronización de datos, bridges entre sistemas legacy y modernos. | Complejidad exponencial. Riesgo de loops de traducción o inconsistencia. |
| Default / Pluggable | Adaptador con valores por defecto. Permite registrar múltiples Adaptee en runtime. | Plugins, proveedores de pago, auth providers (OAuth, SAML, LDAP). | Overhead de registro. Requiere validación de contratos en carga. |
| API Wrapper / Facade Hybrid | Adapta múltiples endpoints en una interfaz cohesiva de alto nivel. | Unificar microservicios, SDKs fragmentados o protocolos mixtos (gRPC/REST). | Puede violar Single Responsibility si acumula demasiada lógica de mapeo. |
| Lazy / Proxy Adapter | Inicializa Adaptee solo cuando se invoca el primer método. | Recursos costosos, conexiones pesadas, carga bajo demanda. | Retrasa fallos de configuración. Dificulta diagnóstico temprano. |
5. 🎯 Cuándo Usar y Cuándo Evitar
| ✅ Usar cuando… | ❌ Evitar cuando… |
|---|---|
| Integras librerías, SDKs o APIs con contratos incompatibles con tu dominio | El Adaptee ya cumple el contrato esperado. Usa la referencia directa. |
| Necesitas unificar múltiples proveedores bajo una interfaz única | La traducción es trivial (solo renombra 1-2 métodos). Usa alias o wrappers simples. |
| Migra gradualmente de un sistema legacy a uno moderno sin downtime | El adaptador requiere lógica de negocio compleja o validaciones cruzadas extensas. |
| Aíslas tu código de breaking changes de vendors externos | El overhead de indirección impacta rendimiento en loops de alta frecuencia (ej. trading, gaming). |
| Normalizas formatos de datos, errores o convenciones de paginación | Ya usas un contenedor DI con factories dinámicas que gestionan la selección. |
Comparación rápida con patrones estructurales:
- Adapter: Traduce interfaces incompatibles. Enfocado en compatibilidad y aislamiento de vendors.
- Facade: Simplifica interfaces complejas. Enfocado en reducir acoplamiento y exponer APIs de alto nivel.
- Decorator: Añade comportamiento dinámicamente. Enfocado en extensión sin modificar estructura base.
- Bridge: Separa abstracción de implementación. Enfocado en variación independiente en dos dimensiones.
- Proxy: Controla acceso o añade lógica pre/post. Enfocado en seguridad, caching o lazy loading.
6. 🧪 Testing, Mantenibilidad y Arquitectura
- Aislamiento en Tests: El
Adapteres el punto ideal para mocks. Prueba el mapeo de entrada/salida sin tocar elAdapteereal.- Técnica: Inyecta un
MockAdapteeque retorna payloads controlados. Verifica queAdaptertransforme correctamente formatos y errores.
- Técnica: Inyecta un
- Validación de Contratos: Escribe tests de integración que invoquen cada método del
Targetcon datos límite, nulos, estructuras vacías y errores simulados.- Fix:
assert.throws(() = > adapter.process(invalidData), /ValidationError/). Valida mensajes, no solo que falle.
- Fix:
- Ciclo de Vida y Ownership: El
Adapterdebe gestionar conexiones, timeouts y reconexiones delAdaptee. Nunca delegue gestión de recursos críticos al cliente. - Refactorización hacia Inyección: Reemplace instanciación manual por contenedores DI. Permita configuración por ambiente (
staging,prod) sin tocar código de adaptación. - Impacto en Rendimiento: La delegación añade 1-2 saltos de función. En CPUs modernas, la JIT los inlina. Solo impacta en millones de llamadas/seg. En ese caso, evalúe caching de respuestas o adaptadores inline.
- Gestión de Versiones: Cuando
Adapteeevoluciona, mantenga la interfazTargetestable. UseAdapterV2paralelo hasta la migración completa. Nunca rompa el contrato deTarget. - Visibilidad y APIs Públicas: Exponga solo
Target. OculteAdapteryAdapteeen módulos internos. Reduzca la superficie de uso indebido y acoplamiento accidental. - Documentación de Mapeo: Especifique explícitamente en comentarios o docs qué campos se transforman, cómo se manejan errores, y qué convenciones se aplican (zonas horarias, decimales, enums).
- Migración desde Uso Directo: Identifique llamadas a
Adapteeen el código. Extraiga lógica de negocio. Envuelva enAdapter. Deprecar acceso directo progresivamente. - Integración con Circuit Breakers: Envuelva el
Adaptercon patrones de resiliencia (retry,fallback,timeout). Aísle fallos de proveedores externos sin propagar al cliente.
7. ⚠️ Errores Comunes y Soluciones
- Abstracción Permeable (Leaky Adapter): El
Adapterexpone tipos, errores o estructuras internas delAdaptee.- Fix: Capture excepciones del
Adaptee, mapee a errores de dominio, y nunca rethrow raw exceptions. Mantenga el contrato puro.
- Fix: Capture excepciones del
- Lógica de Negocio en el Adaptador: El
Adaptercalcula descuentos, valida reglas de negocio o decide flujos.- Fix: El
Adaptersolo traduce y delega. Delegue validaciones y reglas a servicios inyectados. Mantenga responsabilidad única.
- Fix: El
- Acoplamiento Rígido por Herencia: Usar
Class Adapteren lenguajes que no soportan herencia múltiple o mix-ins seguros.- Fix: Prefiera composición. Inyecte
Adapteeen constructor. Permite mockear, reemplazar y testear aisladamente.
- Fix: Prefiera composición. Inyecte
- Fallos Silenciosos en Mapeo: Campos faltantes, tipos incorrectos o fechas mal formateadas pasan sin validación.
- Fix: Valide estructura de respuesta antes de retornar. Use schemas (
zod,valibot,Pydantic) o asserts en modo desarrollo.
- Fix: Valide estructura de respuesta antes de retornar. Use schemas (
- Timeouts y Retries No Gestionados: El
Adapterdelega sin controlar latencia, reintentos o fallos de red.- Fix: Envuelva llamadas con
AbortController,retrypolicies, o circuit breakers. Documente SLA esperado en contratos.
- Fix: Envuelva llamadas con
- Duplicación de Adaptadores: Múltiples
Adapterpara el mismoAdapteeen distintos módulos. Inconsistencia de mapeo.- Fix: Centralice en un módulo compartido. Registre en DI contenedor. Valide que solo exista una implementación por
Target.
- Fix: Centralice en un módulo compartido. Registre en DI contenedor. Valide que solo exista una implementación por
- Confundir con Facade: Usar
Adapterpara simplificar APIs complejas en lugar de traducir interfaces incompatibles.- Fix: Si el
Adapteeya es compatible pero es complejo, use Facade. Si los contratos no coinciden, use Adapter. No mezcle propósitos.
- Fix: Si el
- Serialización Rota: El
Adapterretorna objetos con referencias circulares o campos no serializables.- Fix: Implemente
toJSON()/toString()seguro. Valide estructura antes de exponer. Marque campos internos comotransient.
- Fix: Implemente
- Estado por Defecto Peligroso: Usar
null,"", o0como fallbacks que el sistema acepta pero maneja incorrectamente.- Fix: Use
Optional/Maybe, lanceDomainError, o retorne valores explícitos. Documente comportamiento de fallback.
- Fix: Use
- Olvidar Invalidar Caché Interno: El
Adaptercachea respuestas delAdapteey no invalida cuando cambian datos remotos.- Fix: Use TTL explícito, invalide por evento, o evite cache en adaptadores síncronos. Prefiera cache en capa de aplicación.
8.
Mejores Prácticas y Consejos
- Prefiera Composición sobre Herencia: La delegación explícita es más testeable, flexible y segura. Evite acoplamiento rígido y permita reemplazo dinámico.
- Valide Agresivamente en los Bordes: Rechace payloads malformados inmediatamente. Fail fast ahorra horas de debugging en producción.
- Separe Traducción de Lógica: El
Adaptersolo mapea, transforma y delega. Delegue cálculos, reglas de dominio y orquestación a servicios inyectados. - Documente el Contrato de Mapeo: Especifique qué campos se transforman, cómo se manejan errores, y qué convenciones se aplican. Elimine suposiciones implícitas.
- Use Schemas para Validación:
zod,valibot,Pydantico equivalentes garantizan que las respuestas delAdapteecumplan el contrato antes de retornar. - Implemente Circuit Breakers y Retries: Aísle fallos de proveedores externos. Evite propagar timeouts o errores 5xx al cliente final.
- Pruebe Casos Límite y Errores: Valide respuestas vacías, nulas, campos faltantes, límites numéricos, caracteres especiales y errores simulados del
Adaptee. - Mantenga Contratos Estables: Cambiar la firma de
Targetrompe todos los clientes. Use deprecación controlada, versionado semántico y adapters paralelos. - Monitoree Latencia y Tasa de Error: Registre métricas de adaptación. Detecte degradación de proveedores, mapeos rotos o cuellos de botella en traducción.
- No lo use “por conveniencia”: Si el
Adapteeya es compatible, úselo directo. La capa de adaptación innecesaria es deuda técnica de rendimiento y mantenimiento.
Este cheatsheet proporciona una referencia arquitectónica completa para el patrón Adapter, cubriendo su intención estructural, contratos de traducción seguros, implementación multi-paradigma, variantes de composición y delegación, impacto real en testing y mantenibilidad, errores frecuentes en producción y estrategias de mitigación, junto con criterios estrictos para decidir cuándo la adaptación es una necesidad del dominio y cuándo migrar hacia fachadas, proxies o inyección de dependencias más escalables.