🔌 El Patrón de Diseño Adapter: El Traductor Universal
El patrón de diseño Adapter, podría ser de los patrones más usados y quizá de los más sencillos de entender. Y es que lo usas en tu vida real más de lo que crees.
Yo por si no lo sabías, soy europeo. Por estandarización, en Europa, en la mayoría de países usamos un enchufe concreto para conectar aparatos eléctricos a la red, pero si viajo a Londres (UK) voy a comprobar que ese enchufe es totalmente distinto y cuando vaya a cargar el smartphone no voy a poder conectarlo a la red eléctrica por incompatibilidad. Pues existen artilugios preparados por un lado, para conectar a la red electrica de UK, y justo detrás en este aparato tiene un conector hembra para tu poder conectar lo que tu quieras con tu cargador europeo, y esto tal cuál sería un patrón Adapter como en el software.
Un Poco de Historia 📖
El Adapter fue catalogado por la Gang of Four (GoF) en su libro “Design Patterns” de 1994 como un patrón estructural. A diferencia de los patrones creacionales (como Builder o Factory), los estructurales se preocupan de cómo se componen las clases y objetos para formar estructuras más grandes.
La idea no era nueva: en ingeniería eléctrica y electrónica, los adaptadores existen desde hace más de un siglo. La GoF simplemente formalizó este concepto tan intuitivo y lo llevó al mundo del software. Y es que el problema que resuelve es universal: tienes una cosa que funciona, tienes otra cosa que funciona, pero no se entienden entre sí. Necesitas un traductor.
¿Qué tipo de patrón de diseño es el Adapter?
El Adapter es tan fundamental que muchos frameworks lo usan internamente sin que lo sepas. Cada vez que Laravel envuelve una librería externa en un “driver” o Symfony crea un “bridge” para integrar componentes de terceros, están usando Adapters.
El Adaptador en Acción 🚨
El problema que he comentado anteriormente es una de las primeras cosas que vas a leer en un blog de viajes o similares al viajar a UK. Para estos casos venden un adaptador que lo que hace es justamente lo que hace el patrón de diseño Adapter.
El Adaptador (Adapter) es el objeto que tiene la clavija estandarizada en UK para conectarlo a la red eléctrica de allí, y en la otra parte tiene una clavija para conectar el cargador smartphone con estandarización europea.
El Adaptador no modifica el cargador de móvil ni el enchufe de la pared. Simplemente traduce la interfaz del cargador a la interfaz que la conexión eléctrica de la pared espera, permitiendo poder conectar tu cargador a la electricidad. Es decir, el adaptador se encarga de conectar tu cargador de dos palitos al enchufe de 3 palitos de la pared.
Los Roles del Patrón 🎭
Antes de meternos en código, vamos a dejar claros los roles. Porque aquí cada uno tiene su papel y si lo entiendes bien, el patrón te sale solo:
Tu código. El que necesita usar una funcionalidad pero solo sabe hablar un "idioma" (interfaz concreta).
Ejemplo: Tu sistema que solo sabe llamar a leerPDF()
El contrato que el Cliente espera. Define los métodos que tu código sabe llamar.
Ejemplo: La interfaz GeneradorGraficos
La clase que tiene la funcionalidad que necesitas, pero con una interfaz diferente. No puedes (o no debes) modificarla.
Ejemplo: LibreriaGraficosAntigua con mostrarDiagrama()
El traductor. Implementa la Interfaz Objetivo pero por dentro llama a los métodos del Incompatible.
Ejemplo: AdaptadorGraficosAntiguos
Definición en Programación
En programación es lo mismo al final, el Patrón Adapter es una estructura que permite que dos objetos o clases incompatibles trabajen juntos, actuando como un traductor entre sus interfaces. Vamos, que mediante la función spam de la interfaz SpammerInterface puedas enviar mensajes con la API de Telegram y la de WhatsApp usando la misma función spam e implementando en cada caso la llamada a la respectiva función en cada SDK.
¿Cuál es la responsabilidad principal del Adapter?
Adapter por Composición vs. por Herencia 🔀
Hay dos formas de implementar un Adapter, y es importante conocer ambas porque la elección afecta a la flexibilidad y mantenibilidad de tu código:
El adaptador contiene una instancia del objeto incompatible y delega las llamadas. Es la forma preferida.
$this->libreriaAntigua->mostrarDiagrama()
El adaptador hereda del objeto incompatible. Menos flexible porque te acoplas a una clase concreta.
class Adapter extends Incompatible implements Target
En PHP (y en la mayoría de lenguajes modernos), la composición es la reina. ¿Por qué? Porque PHP no soporta herencia múltiple, y porque “favorece la composición sobre la herencia” es uno de los principios más repetidos en diseño de software. Todos los ejemplos de este post usan composición.
¿Cuál es la diferencia entre Object Adapter y Class Adapter?
💡 Ejemplos Sencillos (Analogía y PHP)
Analogía para Dummies: El Traductor de Documentos
Imagina que tienes un sistema que solo sabe leer documentos en formato PDF. Y ahora tu jefe te pide que el programa tiene que poder leer un documento en formato DOCX (Microsoft Word).
- Tu Sistema (El Cliente): Solo sabe llamar a
leerPDF(). - El Documento DOCX (El Incompatible): Solo tiene un método llamado
abrirDocx(). - El Adaptador (El Traductor): Creas un Adaptador que implementa la interfaz que tu sistema espera (
leerPDF()), pero internamente, cuando lo llamas, lo que hace es llamar al métodoabrirDocx()del documento DOCX.
| Rol | Analogía (Explicación Simple) | ¿Qué Hace en Programación? |
|---|---|---|
| Cliente | Tu sistema (solo pide PDF). | Llama a un método que espera (procesarDatos()). |
| Interfaz Objetivo | El formato que necesita el Cliente (PDF). | Define el método que el Cliente llama. |
| Incompatible | La clase que tiene el formato extraño (DOCX). | Tiene el método real, pero con un nombre diferente (obtenerInfo()). |
| Adaptador | El Traductor. | Implementa la Interfaz Objetivo, pero llama a los métodos del Incompatible. |
Ejemplo en PHP (Para Entender la Traducción)
Imagina que tienes un sistema de informes que solo sabe dibujar gráficos usando una biblioteca antigua. Ahora, quieres usar una nueva librería de gráficos más moderna.
<?php
// 1. Interfaz Objetivo (Lo que tu sistema ESPERA)
interface GeneradorGraficos
{
public function dibujarReporte();
}
// 2. Clase Incompatible (Librería Externa - La que NO encaja)
class LibreriaGraficosAntigua
{
public function mostrarDiagrama()
{
return "Gráfico Antiguo dibujado con método: mostrarDiagrama()";
}
}
// 3. El Adaptador (El Traductor)
class AdaptadorGraficosAntiguos implements GeneradorGraficos
{
public function __construct(
private LibreriaGraficosAntigua $libreriaAntigua
) {}
// Aquí es donde ocurre la MAGIA:
// Implementamos el método esperado (dibujarReporte)...
public function dibujarReporte()
{
// ... y lo 'traducimos' llamando al método que realmente existe.
return "ADAPTADO: " . $this->libreriaAntigua->mostrarDiagrama();
}
}
// 4. El Cliente (Tu código, que no sabe nada de LibreriaGraficosAntigua)
function generarInforme(GeneradorGraficos $grafico)
{
echo "Cliente llama a: " . $grafico->dibujarReporte() . "\n";
}
// USO:
$libreriaVieja = new LibreriaGraficosAntigua();
// Creamos el adaptador para que la vieja librería parezca una nueva
$adaptador = new AdaptadorGraficosAntiguos($libreriaVieja);
// El cliente puede usarlo sin problemas, porque el adaptador "traduce"
generarInforme($adaptador);
// Salida: Cliente llama a: ADAPTADO: Gráfico Antiguo dibujado con método: mostrarDiagrama()
🏢 Ejemplo que se Podría Dar en un Caso Real
El patrón Adapter es muy común cuando se trabaja con APIs y servicios externos, por ejemplo.
Caso Común: Integración de Pagos
Imagina que tu plataforma de comercio electrónico tiene que aceptar pagos:
- Tu Interfaz Objetivo: Tienes una interfaz que defines tú:
ProcesadorPagoscon un métodoejecutarPago(monto). - Proveedores Incompatibles: Quieres usar Stripe, PayPal, y el banco Paco.
- Stripe tiene un método llamado
chargeCustomer($amount). - PayPal tiene un método llamado
processTransaction($price). - El Banco Paco tiene un método llamado
pagamePremo($jurdeles)
- Stripe tiene un método llamado
- Los Adaptadores:
- Creas un
AdaptadorStripeque implementaProcesadorPagos. Cuando llamas a su métodoejecutarPago(monto), internamente llama aStripe->chargeCustomer($monto). - Creas un
AdaptadorPayPalque implementaProcesadorPagos. Cuando llamas a su métodoejecutarPago(monto), internamente llama aPayPal->processTransaction($monto). - Creas un
AdaptadorPacoque implementaProcesadorPagos. Cuando llamas a su métodoejecutarPago(monto), internamente llama aBancoPaco->pagamePremo($monto).
- Creas un
Resultado: Tu código principal solo ve y usa ejecutarPago(monto), sin importarle si el pago se procesa con Stripe, PayPal o el banco del Paco. Si mañana integras otro banco, solo tienes que crear un nuevo Adapter, sin tocar el código central de tu tienda.
Vamos a verlo en código que es como mejor se entiende:
<?php
// Interfaz Objetivo
interface ProcesadorPagos
{
public function ejecutarPago(float $monto): string;
}
// Clases Incompatibles (SDKs de terceros)
class StripeSDK
{
public function chargeCustomer(float $amount): string
{
return "Stripe: Cobrados {$amount}€ al cliente";
}
}
class PayPalSDK
{
public function processTransaction(float $price): string
{
return "PayPal: Transacción de {$price}€ procesada";
}
}
class BancoPacoSDK
{
public function pagamePremo(float $jurdeles): string
{
return "Banco Paco: Te he quitao {$jurdeles}€, illo";
}
}
// Adaptadores
class AdaptadorStripe implements ProcesadorPagos
{
public function __construct(
private StripeSDK $stripe
) {}
public function ejecutarPago(float $monto): string
{
return $this->stripe->chargeCustomer($monto);
}
}
class AdaptadorPayPal implements ProcesadorPagos
{
public function __construct(
private PayPalSDK $paypal
) {}
public function ejecutarPago(float $monto): string
{
return $this->paypal->processTransaction($monto);
}
}
class AdaptadorBancoPaco implements ProcesadorPagos
{
public function __construct(
private BancoPacoSDK $bancoPaco
) {}
public function ejecutarPago(float $monto): string
{
return $this->bancoPaco->pagamePremo($monto);
}
}
// Cliente: tu tienda online
function procesarCompra(ProcesadorPagos $procesador, float $monto): void
{
echo $procesador->ejecutarPago($monto) . "\n";
}
// Uso: cambiar de proveedor es cambiar una línea
$stripe = new AdaptadorStripe(new StripeSDK());
$paypal = new AdaptadorPayPal(new PayPalSDK());
$paco = new AdaptadorBancoPaco(new BancoPacoSDK());
procesarCompra($stripe, 49.99); // Stripe: Cobrados 49.99€ al cliente
procesarCompra($paypal, 49.99); // PayPal: Transacción de 49.99€ procesada
procesarCompra($paco, 49.99); // Banco Paco: Te he quitao 49.99€, illo
Tu función procesarCompra no tiene ni idea de si está hablando con Stripe, PayPal o el banco del Paco. Solo sabe que le pasas algo que cumple con ProcesadorPagos y listo. Eso es programar contra interfaces, y el Adapter es la pieza que lo hace posible cuando las interfaces no coinciden.
En el ejemplo de pagos, ¿qué rol cumple la interfaz ProcesadorPagos?
Otro Ejemplo Real: Logging Multi-Driver 📝
Otro caso muy típico es cuando quieres poder registrar logs en diferentes destinos: archivo, base de datos, Slack, etc.
<?php
interface Logger
{
public function log(string $nivel, string $mensaje): void;
}
// Librería externa de Slack
class SlackWebhook
{
public function sendNotification(string $channel, string $text): void
{
echo "Slack [{$channel}]: {$text}\n";
}
}
// Adapter para que Slack funcione como un Logger
class SlackLoggerAdapter implements Logger
{
public function __construct(
private SlackWebhook $slack,
private string $canal = '#errores'
) {}
public function log(string $nivel, string $mensaje): void
{
$emoji = match($nivel) {
'error' => '🔴',
'warning' => '🟡',
'info' => '🔵',
default => '⚪'
};
$this->slack->sendNotification(
$this->canal,
"{$emoji} [{$nivel}] {$mensaje}"
);
}
}
// Tu código solo conoce la interfaz Logger
function reportarError(Logger $logger, string $mensaje): void
{
$logger->log('error', $mensaje);
}
$slackLogger = new SlackLoggerAdapter(new SlackWebhook(), '#produccion');
reportarError($slackLogger, 'La base de datos no responde');
// Slack [#produccion]: 🔴 [error] La base de datos no responde
Lo guapo de esto es que puedes tener un FileLoggerAdapter, un DatabaseLoggerAdapter y el SlackLoggerAdapter, y tu código sigue llamando a $logger->log() sin enterarse de nada.
En el ejemplo del Logger con Slack, ¿por qué el adaptador recibe el SlackWebhook por el constructor en vez de instanciarlo dentro?
Adapter vs. Otros Patrones Similares 🆚
El Adapter se confunde mucho con otros patrones que también envuelven código. Aquí va la chuleta para que no te líes nunca más:
| Patrón | Propósito | ¿Cuándo usarlo? |
|---|---|---|
| Adapter | Traduce una interfaz a otra compatible | Cuando necesitas integrar código que ya existe pero no encaja con tu interfaz |
| Facade | Simplifica una interfaz compleja | Cuando quieres ocultar la complejidad de un subsistema tras una API simple |
| Decorator | Añade funcionalidad extra | Cuando quieres extender el comportamiento sin modificar la clase original |
| Proxy | Controla el acceso al objeto real | Cuando necesitas lazy loading, caché, control de permisos, etc. |
| Bridge | Separa abstracción de implementación | Cuando tienes múltiples dimensiones de variación (ej: formas × colores) |
El truco para distinguirlos: El Adapter no cambia la funcionalidad, solo la traduce. El Decorator añade funcionalidad. El Facade simplifica la interacción. El Proxy controla el acceso.
¿Qué patrón se confunde frecuentemente con el Adapter pero tiene un propósito diferente?
Adapter y los Principios SOLID 🏛️
El Adapter se lleva de maravilla con SOLID. Mira por qué:
El Adapter tiene una única responsabilidad: traducir entre dos interfaces. No hace lógica de negocio, no valida, no transforma datos. Solo traduce.
¿Nuevo proveedor de pagos? Creas un nuevo Adapter. Sin modificar ni tu código cliente ni los demás adapters. Abierto a extensión, cerrado a modificación.
Todos los Adapters implementan la misma interfaz, así que son intercambiables. Puedes meter un AdaptadorStripe donde antes había un AdaptadorPayPal sin romper nada.
Tu código depende de la interfaz (ProcesadorPagos), no de implementaciones concretas (Stripe, PayPal). El Adapter hace posible esta inversión.
¿Qué principio SOLID aplicas directamente al usar Adapter para integrar un nuevo proveedor de pagos sin tocar el código existente?
✅ Ventajas y ❌ Desventajas
- Compatibilidad entre interfaces diferentes
- Aísla tu código del código de terceros
- Reutilizas clases sin modificarlas
- Código cliente limpio y desacoplado
- Fácil de testear (mockeas la interfaz)
- Cumple con los principios SOLID
- No resuelve bugs, solo traduce interfaces
- Más clases y archivos en el proyecto
- Impacto mínimo en rendimiento (llamada indirecta)
- Puede parecer un "parche" si el diseño inicial fue malo
- Overkill si la interfaz ya coincide
⏰ Cuándo Usarlo y Cuándo Evitarlo
¿Cuándo Usarlo?
- Integrar Código de Terceros: Cuando usas una librería o API externa (como un servicio de clima, pagos, o mapas) y su forma de llamar a los métodos no coincide con la forma en que tu aplicación quiere llamarlos.
- Actualizar Sistemas: Cuando quieres reemplazar una clase vieja por una nueva con mejor funcionalidad, pero no quieres cambiar un montón de líneas de código que dependen de la clase vieja. Creas un Adaptador que hace que la nueva clase parezca la vieja.
- Herencia Compleja: Cuando tienes varias clases existentes que hacen cosas similares pero no tienen una superclase común, y quieres tratarlas de manera uniforme. El Adaptador proporciona esa uniformidad.
- Testing: Cuando necesitas mockear un servicio externo en tus tests. El Adapter te da una interfaz limpia que puedes sustituir fácilmente por un mock.
¿Cuándo NO Usarlo?
- Para solucionar Lógica: Si el problema es que el código no funciona o devuelve resultados incorrectos, el Adaptador no te ayuda. Solo traduce.
- Cuando la Interfaz SÍ Coincide: Si la librería de terceros ya tiene métodos con los nombres que tú necesitas, usar un Adaptador es añadir complejidad innecesaria.
- Solucionar Problemas Menores: No lo uses para cambiar un método de
obtenerDatos()agetDatos(). Un simple cambio de nombre en tu código es más fácil si solo es un lugar. Úsalo cuando el cambio debe ser global para todo tu código cliente.
Errores Comunes al Implementarlo
El Facade simplifica una interfaz compleja. El Adapter traduce una interfaz incompatible. Ambos envuelven código, pero el propósito es muy distinto.
El Adapter solo traduce. Si empiezas a meter validaciones, transformaciones o lógica de negocio dentro, estás creando un monstruo que viola SRP.
Si la librería ya implementa una interfaz compatible o puedes hacer que la implemente, no necesitas Adapter. No crees capas que no aportan.
¿Cuándo NO deberías usar el patrón Adapter?
Adapter en el Mundo Real: Donde Ya Lo Usas 🌍
Los Drivers de Mail, Queue, Cache, Storage... todos son Adapters que traducen tu código a la API de cada proveedor (SES, Redis, S3...).
Los Bridges de Symfony (Doctrine Bridge, Twig Bridge) son Adapters puros que integran librerías externas con el framework.
Los estándares PSR de PHP son interfaces. Las librerías como Guzzle implementan Adapters para cumplirlas.
jQuery.ajax() fue un Adapter gigante que traducía las diferencias entre XMLHttpRequest de cada navegador a una API unificada.
Doctrine, Eloquent, Hibernate... todos adaptan la interfaz SQL de diferentes bases de datos (MySQL, PostgreSQL, SQLite) a una API unificada.
Los SDK clients de APIs externas (Stripe PHP, AWS SDK) son básicamente Adapters que traducen HTTP a llamadas PHP.
EA, nos vemos en los bares! 🍺 saluditos