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 Java, mientras que el adapter por clase se reserva para lenguajes como C++.
Veamos un ejemplo en Java 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.
public interface ServicioPagos {
void cobrar(String clienteId, double monto);
}
public class PasarelaInterna implements ServicioPagos {
@Override
public void cobrar(String clienteId, double monto) {
System.out.println("Cobrando " + monto + " a " + clienteId + " con la pasarela interna");
}
}
// Clase incompatible que queremos reutilizar
public class ProveedorExterno {
public void procesarPago(String correo, double cantidad) {
System.out.println("Procesando pago para " + correo + " con proveedor externo");
}
}
// Adapter por objeto
public class ProveedorExternoAdapter implements ServicioPagos {
private final ProveedorExterno proveedor;
public ProveedorExternoAdapter(ProveedorExterno proveedor) {
this.proveedor = proveedor;
}
@Override
public void cobrar(String clienteId, double monto) {
proveedor.procesarPago(clienteId + "@correo.com", monto);
}
}
El cliente puede alternar entre implementaciones sin conocer los detalles del proveedor:
public class Checkout {
private final ServicioPagos servicioPagos;
public Checkout(ServicioPagos servicioPagos) {
this.servicioPagos = servicioPagos;
}
public void completarCompra(String clienteId, double monto) {
servicioPagos.cobrar(clienteId, monto);
}
public static void main(String[] args) {
ServicioPagos pagosInternos = new PasarelaInterna();
ServicioPagos pagosExternos = new ProveedorExternoAdapter(new ProveedorExterno());
Checkout checkout = new Checkout(pagosExternos);
checkout.completarCompra("cliente123", 2500);
}
}
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.
public interface Notificador {
void enviar(String destino, String mensaje);
}
public class NotificadorEmail implements Notificador {
@Override
public void enviar(String destino, String mensaje) {
System.out.println("Email a " + destino + ": " + mensaje);
}
}
// SDK externo
public class ServicioPush {
public void push(String token, String titulo, String cuerpo) {
System.out.println("Push a " + token + " - " + titulo + ": " + cuerpo);
}
}
public class ServicioPushAdapter implements Notificador {
private final ServicioPush servicioPush;
public ServicioPushAdapter(ServicioPush servicioPush) {
this.servicioPush = servicioPush;
}
@Override
public void enviar(String destino, String mensaje) {
servicioPush.push(destino, "Notificación", mensaje);
}
}
public class GestorNotificaciones {
private final List<Notificador> notificadores = new ArrayList<>();
public void registrar(Notificador notificador) {
notificadores.add(notificador);
}
public void difundir(String mensaje) {
notificadores.forEach(n -> n.enviar(obtenerDestino(n), mensaje));
}
private String obtenerDestino(Notificador notificador) {
// En un sistema real se calcularía en función del notificador
return notificador instanceof ServicioPushAdapter ? "token-123" : "usuario@example.com";
}
public static void main(String[] args) {
GestorNotificaciones gestor = new GestorNotificaciones();
gestor.registrar(new NotificadorEmail());
gestor.registrar(new ServicioPushAdapter(new ServicioPush()));
gestor.difundir("La versión 2.0 ya está 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.