Puente entre Interfaces Incompatibles con el Adapter Pattern
Cómo integrar APIs legacy, librerías de terceros e interfaces incompatibles usando object adapters, class adapters y facade adapters en Java, TypeScript y Python.
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.
Visión general
Tu aplicación espera una interfaz PaymentProcessor con métodos charge(amount) y refund(transactionId). El SDK de Stripe usa charges.create({ amount }) y refunds.create({ charge }). El SDK de PayPal usa orders.capture({ amount }) y payments.refund({ captureId }). Ninguno coincide con tu interfaz. Podrías esparcir código específico de Stripe y PayPal por todo tu codebase, pero cambiar de proveedor requeriría tocar cada archivo que procesa pagos.
El adapter pattern resuelve esto introduciendo una clase wrapper que implementa la interfaz de tu aplicación y traduce las llamadas al SDK de terceros. Tu código de negocio depende solo de la interfaz del adapter. Cambiar Stripe por PayPal significa escribir un nuevo adapter — sin cambios en la lógica de negocio. El siguiente enfoque cubre object adapters, class adapters, two-way adapters y adapter registries con ejemplos prácticos.
Cuándo usarlo
Usa esta receta cuando:
- Integrando una librería de terceros con una interfaz incompatible. Consulta Arquitectura Hexagonal para aislamiento de ports/adapters.
- Migrando desde un sistema legacy sin reescribir código dependiente. Consulta Factory Pattern para crear instancias de adapters.
- Exponiendo una fachada simplificada sobre un subsistema complejo
- Soportando múltiples implementaciones de la misma capacidad (pagos, storage, mensajería). Consulta Strategy Pattern para selección de algoritmos en runtime.
- Testeando código que depende de servicios externos adaptando mocks
Solución
Object Adapter (TypeScript)
interface PaymentProcessor {
charge(amount: number, currency: string): Promise<string>;
refund(transactionId: string): Promise<void>;
}
class StripeSDK {
async createCharge(params: { amount: number; currency: string }) {
return { id: 'ch_' + Math.random().toString(36) };
}
async createRefund(params: { charge: string }) {}
}
class StripeAdapter implements PaymentProcessor {
constructor(private stripe: StripeSDK) {}
async charge(amount: number, currency: string): Promise<string> {
const result = await this.stripe.createCharge({ amount: amount * 100, currency });
return result.id;
}
async refund(transactionId: string): Promise<void> {
await this.stripe.createRefund({ charge: transactionId });
}
}
class CheckoutService {
constructor(private processor: PaymentProcessor) {}
async process(order: Order): Promise<void> {
const txId = await this.processor.charge(order.total, order.currency);
await this.orderRepo.save({ ...order, transactionId: txId });
}
}
Class Adapter (Java)
interface ModernLogger {
void log(String level, String message);
}
class LegacyLogger {
public void writeLogEntry(String entry) {
System.out.println("[LEGACY] " + entry);
}
}
class LoggerAdapter extends LegacyLogger implements ModernLogger {
@Override
public void log(String level, String message) {
writeLogEntry(String.format("[%s] %s", level.toUpperCase(), message));
}
}
ModernLogger logger = new LoggerAdapter();
logger.log("info", "Application started");
Python Adapter con Registry
from abc import ABC, abstractmethod
from typing import Dict
class StorageAdapter(ABC):
@abstractmethod
def upload(self, key: str, data: bytes) -> str: pass
@abstractmethod
def download(self, key: str) -> bytes: pass
class S3Adapter(StorageAdapter):
def __init__(self, client):
self.client = client
def upload(self, key: str, data: bytes) -> str:
self.client.put_object(Bucket="my-bucket", Key=key, Body=data)
return f"s3://my-bucket/{key}"
def download(self, key: str) -> bytes:
return self.client.get_object(Bucket="my-bucket", Key=key)["Body"].read()
class AzureBlobAdapter(StorageAdapter):
def __init__(self, container_client):
self.container = container_client
def upload(self, key: str, data: bytes) -> str:
self.container.upload_blob(name=key, data=data, overwrite=True)
return f"azure://my-container/{key}"
def download(self, key: str) -> bytes:
return self.container.download_blob(key).readall()
class StorageFactory:
_adapters: Dict[str, type] = {}
@classmethod
def register(cls, name: str, adapter_class: type):
cls._adapters[name] = adapter_class
@classmethod
def create(cls, name: str, config: dict) -> StorageAdapter:
return cls._adapters[name](**config)
StorageFactory.register("s3", S3Adapter)
StorageFactory.register("azure", AzureBlobAdapter)
storage = StorageFactory.create("s3", {"client": boto3_client})
url = storage.upload("report.pdf", pdf_bytes)
Explicación
- Object adapter: el adapter mantiene una referencia al adaptee (la clase de terceros) y delega las llamadas a él. Es el enfoque más flexible — funciona con clases final, soporta composición sobre herencia, y permite adaptar múltiples adaptees simultáneamente.
- Class adapter: Requiere que el adaptee no sea final y funciona solo en lenguajes de herencia simple donde el adapter no extiende otra clase. Es menos flexible pero ligeramente más rápido.
- Two-way adapter: Traduce llamadas en ambas direcciones, actuando como un puente durante migraciones incrementales.
- Adapter registry: cuando se soportan múltiples proveedores (Stripe, PayPal, Braintree), un registro mapea nombres de proveedor a clases adapter. La factory instancia el adapter correcto basado en configuración, aislando la selección del adapter de la lógica de negocio.
Variantes
| Variante | Flexibilidad | Rendimiento | Mejor para |
|---|---|---|---|
| Object adapter | Alta | Medio | Uso general, SDKs de terceros |
| Class adapter | Baja | Alto | Crítico de rendimiento, un solo adaptee |
| Two-way adapter | Media | Medio | Migración incremental |
| Facade adapter | Alta | Medio | Simplificar subsistemas complejos |
| Registry + adapter | Alta | Medio | Soporte de múltiples proveedores |
Lo que funciona
- Adapta en el límite, no en todas partes: introduce adapters en los límites del sistema donde las interfaces externas se encuentran con abstracciones internas. No dejes que los tipos de terceros se filtren a la lógica de negocio.
- Documenta el comportamiento de traducción: los adapters hacen más que renombrar métodos. Pueden convertir unidades, transformar tipos de error, o batch requests.
- Maneja errores con elegancia: las APIs de terceros lanzan excepciones específicas del vendor. El adapter debe capturarlas y mapearlas a la taxonomía de errores de tu aplicación.
StripeCardErrorse convierte enPaymentDeclinedError. - Mantén los adapters delgados: un adapter con cientos de líneas de lógica es un servicio, no un adapter. Las transformaciones complejas pertenecen a servicios de aplicación. El adapter debe traducir llamadas y errores, y luego salir del camino.
- Testea adapters con contract tests: escribe tests que verifiquen que el adapter satisface la interfaz objetivo, no tests que verifiquen el SDK de terceros. Consulta Input Validation para contratos de límite.
Errores comunes
- Filtrar detalles del adaptee: retornar objetos de respuesta nativos del adaptee desde el adapter fuerza a los consumidores a entender la API de terceros. Siempre retorna tipos de dominio desde el adapter.
- Adapter inflado: poner caching, reintentos y métricas dentro del adapter lo hace difícil de testear y reusar.
- No manejar null/undefined: las APIs de terceros pueden retornar
nulldonde tu interfaz espera un objeto vacío o una excepción. Define el contrato de null del adapter y traduce consistentemente. - Acoplamiento fuerte a versiones de SDK: cuando el SDK de terceros lanza un cambio breaking, el adapter lo absorbe. Si llamas al SDK directamente desde código de negocio, cada cambio breaking se propaga por todas partes. El adapter es tu amortiguador de choque.
Preguntas frecuentes
PayPal Adapter y Soporte Multi-Provider
class PayPalSDK {
async captureOrder(params: { amount: number; currency: string }) {
return { id: 'PAYID-' + Math.random().toString(36) };
}
async refundPayment(params: { captureId: string }) {}
}
class PayPalAdapter implements PaymentProcessor {
constructor(private paypal: PayPalSDK) {}
async charge(amount: number, currency: string): Promise<string> {
const result = await this.paypal.captureOrder({ amount, currency });
return result.id;
}
async refund(transactionId: string): Promise<void> {
await this.paypal.refundPayment({ captureId: transactionId });
}
}
// Selección de proveedor vía factory
class PaymentProcessorFactory {
private static providers: Map<string, () => PaymentProcessor> = new Map();
static register(name: string, factory: () => PaymentProcessor) {
this.providers.set(name, factory);
}
static create(name: string): PaymentProcessor {
const factory = this.providers.get(name);
if (!factory) throw new Error(`Unknown provider: ${name}`);
return factory();
}
}
PaymentProcessorFactory.register('stripe', () =>
new StripeAdapter(new StripeSDK()));
PaymentProcessorFactory.register('paypal', () =>
new PayPalAdapter(new PayPalSDK()));
// Uso — cambia proveedores vía config
const processor = PaymentProcessorFactory.create(process.env.PAYMENT_PROVIDER);
Two-Way Adapter para Migración Incremental
// Interfaz vieja — siendo reemplazada
interface LegacyPaymentApi {
processPayment(amount: number): Promise<{ txId: string }>;
}
// Interfaz nueva — arquitectura objetivo
interface ModernPaymentApi {
charge(amount: number, currency: string): Promise<string>;
refund(transactionId: string): Promise<void>;
}
// Two-way adapter — implementa ambas interfaces
class PaymentBridge implements LegacyPaymentApi, ModernPaymentApi {
constructor(private processor: PaymentProcessor) {}
// Interfaz legacy — delega a la nueva
async processPayment(amount: number): Promise<{ txId: string }> {
const txId = await this.processor.charge(amount, 'USD');
return { txId };
}
// Interfaz moderna
async charge(amount: number, currency: string): Promise<string> {
return this.processor.charge(amount, currency);
}
async refund(transactionId: string): Promise<void> {
return this.processor.refund(transactionId);
}
}
// Código viejo puede usar processPayment(), código nuevo usa charge()
// Ambos coexisten durante la migración
In-Memory Adapter para Testing
class InMemoryPaymentAdapter implements PaymentProcessor {
private transactions: Map<string, { amount: number; currency: string }> = new Map();
private refunded: Set<string> = new Set();
async charge(amount: number, currency: string): Promise<string> {
const txId = 'test_' + Math.random().toString(36);
this.transactions.set(txId, { amount, currency });
return txId;
}
async refund(transactionId: string): Promise<void> {
if (!this.transactions.has(transactionId)) {
throw new Error('Transaction not found');
}
this.refunded.add(transactionId);
}
// Helpers de test
getChargedAmount(txId: string): number | undefined {
return this.transactions.get(txId)?.amount;
}
wasRefunded(txId: string): boolean {
return this.refunded.has(txId);
}
}
// Uso en tests
describe('CheckoutService', () => {
it('carga el monto correcto', async () => {
const processor = new InMemoryPaymentAdapter();
const service = new CheckoutService(processor);
const txId = await service.process({
total: 99.99,
currency: 'USD',
});
expect(processor.getChargedAmount(txId)).toBe(99.99);
});
});
Recursos Relacionados
Construir Aplicaciones Mantenibles con Arquitectura
Cómo estructurar aplicaciones usando ports y adapters para aislar lógica de negocio de frameworks, bases de datos y servicios externos para testabilidad y flexibilidad.
RecipeCrear Objetos Flexiblemente con el Factory Pattern
Cómo usar factory methods, abstract factories y containers de inyección de dependencias para desacoplar creación de objetos de su uso y mejorar testeabilidad.
RecipeDiseñar un API Gateway Escalable para Microservicios
Cómo construir un gateway de API que enrute requests, maneje autenticación, rate limiting, caching y traducción de protocolos entre clientes y microservicios backend.