El patrón Adapter permite que dos interfaces incompatibles colaboren sin modificar su código original. Se emplea para envolver un componente existente y presentar una interfaz que el cliente espera, facilitando la reutilización de bibliotecas, sistemas heredados y servicios externos.
En la práctica, Adapter actúa como un traductor: recibe peticiones usando la interfaz objetivo y las transforma en invocaciones hacia interfaces ya disponibles. Esto reduce el acoplamiento y evita duplicar funcionalidad solo para encajar en un formato distinto.
Los proyectos suelen integrar componentes desarrollados en momentos diferentes o por equipos con convenciones particulares. Cambiar la interfaz de una biblioteca para que encaje con nuestro modelo puede ser inviable, ya sea por restricciones de licencia, mantenimiento o estabilidad. Adapter ofrece una capa intermedia que reconcilia ambas visiones.
Este patrón resulta especialmente útil cuando una aplicación evolucionó con clases que ya consumen determinada interfaz y aparece un nuevo proveedor que expone operaciones con nombres, tipos o firmas distintas.
Su intención es convertir la interfaz de una clase en otra interfaz que los clientes esperan. De este modo, las clases incompatibles pueden trabajar juntas sin cambios internos. Se emplea cuando:
El patrón también es fundamental en integraciones siguiendo Domain-Driven Design, donde un anticorruption layer protege el dominio de las inconsistencias externas.
Los participantes clave son:
La implementación concreta puede variar según la modalidad (por objeto o por clase), pero en cualquier caso el adaptador encapsula la conversión de datos y las llamadas necesarias.
Existen dos variantes clásicas:
En la práctica, el adapter por objeto es el más utilizado en C#, mientras que el adapter por clase se reserva para lenguajes como C++.
Veamos un ejemplo en C# donde un sistema espera un servicio de pagos con una interfaz estandarizada, pero un proveedor externo ofrece una API distinta. El adaptador permite reutilizarlo sin cambiar el código del cliente.
using System;
public interface IServicioPagos
{
void Cobrar(string clienteId, decimal monto);
}
public class PasarelaInterna : IServicioPagos
{
public void Cobrar(string clienteId, decimal monto)
{
Console.WriteLine($"Cobrando {monto} a {clienteId} con la pasarela interna");
}
}
// Clase incompatible que queremos reutilizar
public class ProveedorExterno
{
public void ProcesarPago(string correo, decimal cantidad)
{
Console.WriteLine($"Procesando pago para {correo} con proveedor externo");
}
}
// Adapter por objeto
public class ProveedorExternoAdapter : IServicioPagos
{
private readonly ProveedorExterno _proveedor;
public ProveedorExternoAdapter(ProveedorExterno proveedor)
{
_proveedor = proveedor;
}
public void Cobrar(string clienteId, decimal monto)
{
_proveedor.ProcesarPago($"{clienteId}@correo.com", monto);
}
}
El cliente puede alternar entre implementaciones sin conocer los detalles del proveedor:
using System;
public class Checkout
{
private readonly IServicioPagos _servicioPagos;
public Checkout(IServicioPagos servicioPagos)
{
_servicioPagos = servicioPagos;
}
public void CompletarCompra(string clienteId, decimal monto)
{
_servicioPagos.Cobrar(clienteId, monto);
}
public static void Main()
{
IServicioPagos pagosInternos = new PasarelaInterna();
IServicioPagos pagosExternos = new ProveedorExternoAdapter(new ProveedorExterno());
var checkout = new Checkout(pagosExternos);
checkout.CompletarCompra("cliente123", 2500m);
}
}
Imaginemos una aplicación que envía notificaciones mediante una interfaz doméstica. Un proveedor de mensajería moderno ofrece un SDK con nombres y estructuras distintas. Implementamos un adaptador que traduce los datos y permite seleccionar en tiempo de ejecución el proveedor deseado.
using System;
using System.Collections.Generic;
public interface INotificador
{
void Enviar(string destino, string mensaje);
}
public class NotificadorEmail : INotificador
{
public void Enviar(string destino, string mensaje)
{
Console.WriteLine($"Email a {destino}: {mensaje}");
}
}
// SDK externo
public class ServicioPush
{
public void Push(string token, string titulo, string cuerpo)
{
Console.WriteLine($"Push a {token} - {titulo}: {cuerpo}");
}
}
public class ServicioPushAdapter : INotificador
{
private readonly ServicioPush _servicioPush;
public ServicioPushAdapter(ServicioPush servicioPush)
{
_servicioPush = servicioPush;
}
public void Enviar(string destino, string mensaje)
{
_servicioPush.Push(destino, "Notificacion", mensaje);
}
}
public class GestorNotificaciones
{
private readonly List<INotificador> _notificadores = new List<INotificador>();
public void Registrar(INotificador notificador)
{
_notificadores.Add(notificador);
}
public void Difundir(string mensaje)
{
foreach (var notificador in _notificadores)
{
var destino = ObtenerDestino(notificador);
notificador.Enviar(destino, mensaje);
}
}
private string ObtenerDestino(INotificador notificador)
{
return notificador is ServicioPushAdapter ? "token-123" : "usuario@example.com";
}
public static void Main()
{
var gestor = new GestorNotificaciones();
gestor.Registrar(new NotificadorEmail());
gestor.Registrar(new ServicioPushAdapter(new ServicioPush()));
gestor.Difundir("La version 2.0 ya esta disponible");
}
}
El adapter encapsula el SDK externo y evita que el resto del sistema deba conocer su interfaz específica. Esto reduce el impacto de futuras migraciones.
Entre los problemas más comunes destacan:
Una solución habitual consiste en combinar Adapter con Strategy, seleccionando dinámicamente qué adaptador utilizar según el contexto de ejecución.
El patrón aporta valor cuando:
Adapter complementa a Facade y Bridge: una fachada puede usar adaptadores para ofrecer una interfaz simplificada, mientras que Bridge aporta extensibilidad cuando se necesita variar tanto la abstracción como la implementación.