Saltar al contenidoJMRG
Todas las entradas

Patrón Adapter: Integrando Interfaces Incompatibles

design-patternsstructuraladapter

¿Qué es el Patrón Adapter?

El patrón Adapter permite que interfaces incompatibles colaboren. Convierte la interfaz de una clase en otra que el cliente espera, actuando como un traductor entre sistemas con APIs diferentes.

classDiagram
    class Target {
        <<interface>>
        +request()
    }

    class Adapter {
        -adaptee: Adaptee
        +request()
    }

    class Adaptee {
        +specificRequest()
    }

    class Client

    Client --> Target
    Target <|.. Adapter
    Adapter --> Adaptee

Piensa en un adaptador de enchufe: convierte un enchufe europeo a uno americano sin cambiar ninguno de los dos dispositivos.

El Problema que Resuelve

Tu aplicación usa una interfaz estándar, pero cada SDK externo tiene su propia API:

interface PaymentProcessor {
  charge(amount: number, currency: string, token: string): Promise<PaymentResult>;
  refund(transactionId: string, amount?: number): Promise<RefundResult>;
}
class StripeSDK {
  createPaymentIntent(params: { amount: number; currency: string }): Promise<StripeIntent> { }
}
class PayPalAPI {
  createOrder(params: { intent: string; purchase_units: any[] }): Promise<PayPalOrder> { }
}

Sin Adapter: Tendrías que modificar todo el código para cada API diferente.

La Solución: Adapter

1. Definir la Interfaz Target

interface PaymentResult {
  success: boolean;
  transactionId: string;
  amount: number;
  currency: string;
  timestamp: Date;
}
/** Interfaz unificada para procesadores de pago */
interface PaymentProcessor {
  charge(amount: number, currency: string, method: string): Promise<PaymentResult>;
  refund(transactionId: string, amount?: number): Promise<RefundResult>;
}

2. Crear el Adapter para Stripe

/** Adapta el SDK de Stripe a nuestra interfaz PaymentProcessor */
class StripeAdapter implements PaymentProcessor {
  constructor(private stripe: StripeSDK) {}
  async charge(amount: number, currency: string, method: string): Promise<PaymentResult> {
    const stripeAmount = Math.round(amount * 100);
    const intent = await this.stripe.createPaymentIntent({
      amount: stripeAmount,
      currency: currency.toLowerCase(),
      payment_method: method,
      confirm: true,
    });
    return {
      success: intent.status === 'succeeded',
      transactionId: intent.id,
      amount: intent.amount / 100,
      currency: intent.currency.toUpperCase(),
      timestamp: new Date(intent.created * 1000),
    };
  }
  async refund(transactionId: string, amount?: number): Promise<RefundResult> {
    const refund = await this.stripe.createRefund({
      payment_intent: transactionId,
      amount: amount ? Math.round(amount * 100) : undefined,
    });
    return {
      success: refund.status === 'succeeded',
      refundId: refund.id,
      amount: refund.amount / 100,
    };
  }
}

3. Crear el Adapter para PayPal

/** Adapta la API de PayPal a nuestra interfaz PaymentProcessor */
class PayPalAdapter implements PaymentProcessor {
  constructor(private paypal: PayPalAPI) {}
  async charge(amount: number, currency: string, method: string): Promise<PaymentResult> {
    const order = await this.paypal.createOrder({
      intent: 'CAPTURE',
      purchase_units: [{
        amount: { value: amount.toFixed(2), currency_code: currency.toUpperCase() },
      }],
    });
    const capture = await this.paypal.captureOrder(order.id);
    return {
      success: capture.status === 'COMPLETED',
      transactionId: capture.id,
      amount: parseFloat(capture.purchase_units[0].payments.captures[0].amount.value),
      currency: capture.purchase_units[0].payments.captures[0].amount.currency_code,
      timestamp: new Date(),
    };
  }
  async refund(transactionId: string, amount?: number): Promise<RefundResult> {
    const refund = await this.paypal.refundCapture(transactionId, {
      amount: amount ? { value: amount.toFixed(2) } : undefined,
    });
    return {
      success: refund.status === 'COMPLETED',
      refundId: refund.id,
      amount: parseFloat(refund.amount.value),
    };
  }
}

4. Usar los Adapters

/** Servicio de negocio que usa cualquier procesador */
class PaymentService {
  constructor(private processor: PaymentProcessor) {}
  async processPayment(amount: number, currency: string, method: string): Promise<PaymentResult> {
    return this.processor.charge(amount, currency, method);
  }
}
const stripeService = new PaymentService(new StripeAdapter(new StripeSDK('sk_xxx')));
const paypalService = new PaymentService(new PayPalAdapter(new PayPalAPI('id', 'secret')));
const result1 = await stripeService.processPayment(99.99, 'USD', 'pm_xxx');
const result2 = await paypalService.processPayment(99.99, 'USD', 'token_xxx');

Beneficios del Patrón Adapter

Aspecto Beneficio
Desacoplamiento El código cliente no depende de SDKs externos
Single Responsibility Cada adapter maneja una traducción
Open/Closed Agregar proveedores sin modificar código
Testabilidad Fácil de mockear en tests

Cuándo Usar Adapter

  • Integrar librerías de terceros con interfaces incompatibles
  • Migrar de una API legacy a una nueva
  • Crear wrappers para servicios externos
  • Normalizar múltiples fuentes de datos

Cuándo NO Usar Adapter

  • Si la interfaz externa ya es compatible
  • Si solo hay una implementación y nunca cambiará

Conclusión

El patrón Adapter es esencial para integrar sistemas externos de forma limpia. Actúa como un traductor que permite que tu código interno permanezca estable mientras los servicios externos cambian o se agregan nuevos.

Este es el último artículo de nuestra serie introductoria. Te invito a explorar más patrones de diseño y aplicarlos en tus proyectos.


Basado en "Design Patterns: Elements of Reusable Object-Oriented Software" (Gang of Four).