Patrón Adapter: Integrando Interfaces Incompatibles

¿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 --> AdapteePiensa 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).