Software Architecture
Adapter Pattern
Explica Adapter Pattern como una traducción entre contratos incompatibles para aislar proveedores, protocolos y modelos externos sin contaminar el dominio.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Software Architecture
Explica Adapter Pattern como una traducción entre contratos incompatibles para aislar proveedores, protocolos y modelos externos sin contaminar el dominio.
Adapter convierte una interfaz, modelo o protocolo externo en la forma que necesita el consumidor. Su valor no está en envolver una dependencia, sino en concentrar la incompatibilidad y proteger el lenguaje propio.
Dos partes pueden ofrecer capacidades compatibles y, aun así, no poder colaborar directamente porque difieren en:
Adapter introduce una traducción:
consumidor
→ contrato propio
→ adapter
→ sistema externoSin adapter, el consumidor termina conociendo detalles externos:
const intent = await stripe.paymentIntents.create({
amount: order.total * 100,
currency: "cop",
payment_method: methodId,
confirm: true,
});
if (intent.status === "requires_capture") {
// lógica del pedido conoce Stripe
}El dominio ahora depende de:
Un adapter localiza ese conocimiento.
interface PaymentGateway {
authorize(input: AuthorizationRequest): Promise<AuthorizationResult>;
}
type AuthorizationResult =
| { type: "approved"; authorizationId: string }
| { type: "rejected"; reason: string }
| { type: "unknown"; reconciliationId: string };El contrato expresa lo que la aplicación necesita: aprobación, rechazo o resultado desconocido.
class StripePaymentGateway implements PaymentGateway {
constructor(private readonly stripe: Stripe) {}
async authorize(input: AuthorizationRequest): Promise<AuthorizationResult> {
try {
const intent = await this.stripe.paymentIntents.create(
{
amount: input.amount.minorUnits,
currency: input.amount.currency.toLowerCase(),
payment_method: input.paymentMethodId,
confirm: true,
capture_method: "manual",
},
{ idempotencyKey: input.idempotencyKey },
);
if (intent.status === "requires_capture") {
return { type: "approved", authorizationId: intent.id };
}
return { type: "rejected", reason: intent.status };
} catch (error) {
if (isTimeout(error)) {
return {
type: "unknown",
reconciliationId: input.idempotencyKey,
};
}
throw mapStripeConfigurationError(error);
}
}
}En producción se necesitan logs seguros, métricas, firma de webhooks, persistencia del intento y reconciliación. El adapter no debe ocultar incertidumbre real.
DTOs, nombres, fechas, enums y estructuras.
success externo puede no equivaler a operación final. La traducción debe respetar significado.
Pesos frente a centavos, UTC frente a zona local, kilogramos frente a gramos.
Distingue validación, rechazo, timeout, rate limit y configuración inválida.
Convierte callbacks, promesas, webhooks o polling en un modelo manejable.
Relaciona IDs externos con referencias propias e idempotency keys.
Encapsula una dependencia, pero puede mantener la misma interfaz. No necesariamente traduce incompatibilidad.
Ofrece una interfaz simplificada sobre varias capacidades o componentes.
Añade comportamiento conservando el contrato: métricas, caching o autorización.
Controla acceso, ubicación o creación manteniendo una interfaz equivalente.
Conjunto más amplio de adapters y mappers que protege un modelo de dominio frente a otro.
La diferencia práctica es la intención y el tipo de conocimiento que se contiene.
Usa composición:
class LegacyInventoryAdapter implements InventoryPort {
constructor(private readonly legacy: LegacyClient) {}
}Es flexible y permite envolver una instancia.
Usa herencia para adaptar una clase. Depende de que la relación sea estable y el lenguaje permita herencia adecuada. Suele ser menos flexible.
No todos conectan proveedores. Un controller también adapta:
HTTP request
→ command de aplicación
resultado de aplicación
→ HTTP responseDebe traducir protocolo sin contener reglas de negocio.
Mapea entre fila y modelo:
function toOrder(row: OrderRow): Order {
return Order.restore({
id: new OrderId(row.id),
status: mapStatus(row.status),
total: Money.fromMinorUnits(row.total, row.currency),
version: row.version,
});
}Debe validar incompatibilidades. Un estado desconocido no debería convertirse silenciosamente en un valor por defecto.
Dos proveedores rara vez son perfectamente intercambiables.
Proveedor A puede soportar autorización y captura separadas. Proveedor B solo cobro inmediato. Diseñar un contrato genérico de “processPayment” puede ocultar una diferencia crítica.
Opciones:
El adapter reduce acoplamiento técnico, no elimina lock-in semántico o de datos.
Fakes para probar consumidores.
Pruebas contra sandbox o emulador:
Ejecuta una suite común sobre implementaciones para comprobar semántica compartida.
Un mock del SDK solo demuestra que tu código llamó una función, no que el proveedor se comporte como esperas.
Un enum externo puede añadir valores. El adapter debe manejar desconocidos y alertar, no fallar de forma silenciosa.
No asumas que la operación no ocurrió. Devuelve estado desconocido y reconcilia.
Distingue error del consumidor de fallo externo.
El adapter puede soportar versiones durante una migración, pero necesita métricas para retirar la anterior.
La traducción puede incluir batching, límites y backpressure. No escondas restricciones de capacidad.
Una función local y estable puede usarse directamente. El adapter debe proteger una frontera real, no satisfacer un ritual.
Domain-Driven Design estratégico usa límites y traducciones para organizar modelos con significados diferentes dentro de un dominio complejo.