El patrón Proxy define un intermediario que controla el acceso a un objeto real, ya sea para diferir su creación, agregar verificaciones o realizar optimizaciones transversales. El proxy expone la misma interfaz que el objeto representado, por lo que el cliente desconoce si está interactuando con la instancia real o con su apoderado.
Al incorporar un punto de delegación explícito, el patrón permite encapsular aspectos como la seguridad, la distribución remota, la gestión de recursos o la instrumentación sin invadir la lógica del objeto principal. Es una forma de cumplir el principio de responsabilidad única al aislar funciones de soporte en un colaborador especializado.
En diseños reales surgen situaciones donde la instanciación o el uso directo de un objeto es costoso o riesgoso. Acceder a un documento pesado, establecer conexiones remotas o permitir operaciones sensibles son ejemplos que requieren controles adicionales. Introducir esas preocupaciones dentro del objeto concreto genera código intrusivo, viola la cohesión y dificulta pruebas.
Proxy soluciona el problema interponiendo un representante que decide cuándo y cómo llegar al objeto real. El cliente conserva una interfaz uniforme mientras que el apoderado gestiona la vida útil, el acceso concurrente, los permisos o cualquier lógica transversal sin que el objeto real se entere.
La intención del patrón es proporcionar un substituto controlado de otro objeto para mediar sus solicitudes. Es especialmente útil cuando:
El apoderado se convierte en un lugar idóneo para centralizar políticas de acceso, instrumentación y optimizaciones, manteniendo el concepto principal limpio y enfocado en su responsabilidad de negocio.
La estructura clásica del patrón involucra los siguientes participantes:
El proxy y el objeto verdadero comparten la misma interfaz, por lo que la sustitución es transparente. El proxy está autorizado a crear el objeto real bajo demanda, a reutilizarlo o incluso a delegar en otro proxy encadenado si se requiere una cadena de responsabilidades.
A lo largo de la literatura se describen varias especializaciones del patrón:
Un mismo apoderado puede mezclar varias motivaciones: cachear resultados, verificar credenciales y llevar métricas en una sola implementación, siempre que mantenga la claridad y no se convierta en un monolito de responsabilidades.
Supongamos una plataforma de streaming que consulta metadatos en un proveedor remoto con latencia variable. Las cuentas gratuitas solo pueden acceder a trailers, mientras que las cuentas premium consultan el catálogo completo. Además, se desea limitar la cantidad de solicitudes consecutivas para prevenir abusos e incorporar caché para evitar costosas llamadas repetidas.
El patrón Proxy es ideal: un apoderado controla a quién se permite el acceso, regula el ritmo de peticiones, reutiliza datos cacheados y delega en el objeto real cuando la información no está disponible localmente.
El siguiente fragmento muestra la estructura completa del ejemplo anterior:
package tutorial.proxy;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public interface CatalogoVideos {
Video obtenerVideo(String id, Usuario solicitante);
}
class CatalogoVideosRemoto implements CatalogoVideos {
@Override
public Video obtenerVideo(String id, Usuario solicitante) {
simularLatencia();
System.out.println("Consultando proveedor externo para " + id);
return new Video(id, "Documental " + id, 720);
}
private void simularLatencia() {
try {
Thread.sleep(Duration.ofMillis(150).toMillis());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Consulta interrumpida", e);
}
}
}
class CatalogoVideosProxy implements CatalogoVideos {
private final CatalogoVideos remoto;
private final Duration intervaloEntreSolicitudes;
private final Map<String, Video> cache = new ConcurrentHashMap<>();
private final Map<String, Instant> ultimoAccesoPorUsuario = new ConcurrentHashMap<>();
private final Set<String> catalogoPromocional;
CatalogoVideosProxy(CatalogoVideos remoto,
Duration intervaloEntreSolicitudes,
Set<String> catalogoPromocional) {
this.remoto = remoto;
this.intervaloEntreSolicitudes = intervaloEntreSolicitudes;
this.catalogoPromocional = Collections.unmodifiableSet(new HashSet<>(catalogoPromocional));
}
@Override
public Video obtenerVideo(String id, Usuario solicitante) {
validarAcceso(id, solicitante);
controlarRitmo(solicitante);
Video video = cache.get(id);
if (video == null) {
video = remoto.obtenerVideo(id, solicitante);
cache.put(id, video);
}
registrarMetrica(id, solicitante, video);
return video;
}
private void validarAcceso(String id, Usuario solicitante) {
if (!solicitante.esPremium() && !catalogoPromocional.contains(id)) {
throw new AccesoDenegadoException("Actualice su plan para ver el video " + id);
}
}
private void controlarRitmo(Usuario solicitante) {
Instant ahora = Instant.now();
Instant ultimoAcceso = ultimoAccesoPorUsuario.put(solicitante.getNombre(), ahora);
if (!solicitante.esPremium() && ultimoAcceso != null) {
Duration intervalo = Duration.between(ultimoAcceso, ahora);
if (intervalo.compareTo(intervaloEntreSolicitudes) < 0) {
throw new IllegalStateException("Demasiadas solicitudes seguidas de " + solicitante.getNombre());
}
}
}
private void registrarMetrica(String id, Usuario solicitante, Video video) {
System.out.printf("Usuario %s recibió %s (%d MB)%n",
solicitante.getNombre(), video.getTitulo(), video.getPesoMb());
}
}
class AccesoDenegadoException extends RuntimeException {
AccesoDenegadoException(String mensaje) {
super(mensaje);
}
}
class Video {
private final String id;
private final String titulo;
private final long pesoMb;
Video(String id, String titulo, long pesoMb) {
this.id = id;
this.titulo = titulo;
this.pesoMb = pesoMb;
}
public String getId() {
return id;
}
public String getTitulo() {
return titulo;
}
public long getPesoMb() {
return pesoMb;
}
@Override
public String toString() {
return "Video{id='" + id + "', titulo='" + titulo + "', pesoMb=" + pesoMb + '}';
}
}
class Usuario {
private final String nombre;
private final boolean premium;
Usuario(String nombre, boolean premium) {
this.nombre = nombre;
this.premium = premium;
}
public String getNombre() {
return nombre;
}
public boolean esPremium() {
return premium;
}
}
class Aplicacion {
public static void main(String[] args) {
CatalogoVideos servicioRemoto = new CatalogoVideosRemoto();
Set<String> catalogoPromocional = new HashSet<>();
catalogoPromocional.add("VID-TRAILER");
CatalogoVideos proxy = new CatalogoVideosProxy(
servicioRemoto,
Duration.ofSeconds(2),
catalogoPromocional
);
Usuario invitada = new Usuario("Laura", false);
Usuario premium = new Usuario("Sergio", true);
System.out.println(proxy.obtenerVideo("VID-TRAILER", invitada)); // usa caché
try {
proxy.obtenerVideo("VID-ALBUM", invitada); // lanza AccesoDenegadoException
} catch (AccesoDenegadoException ex) {
System.out.println("Mensaje para la interfaz: " + ex.getMessage());
}
System.out.println(proxy.obtenerVideo("VID-ALBUM", premium)); // delega al remoto
System.out.println(proxy.obtenerVideo("VID-ALBUM", premium)); // devuelve desde caché
}
}
El cliente recibe una referencia de tipo CatalogoVideos
e ignora si se trata de un proxy o del objeto concreto. El proxy intercepta cada llamada, valida si el video solicitado está disponible para el tipo de cuenta, comprueba que no se supere la cuota de uso y, solo cuando es necesario, invoca al servicio remoto. Almacena la respuesta en caché y registra una métrica, demostrando cómo encapsular varios aspectos transversales sin recompilar el objeto real.
Este diseño prepara el terreno para reemplazar el servicio remoto por otro proveedor, realizar pruebas unitarias con dobles de prueba o incorporar nuevas reglas de negocio sin tocar la implementación original.
Las bibliotecas y plataformas del JDK emplean proxies con frecuencia. La documentación oficial de RMI describe cómo generar proxies remotos que representan objetos en otras JVM. Del mismo modo, la clase Proxy del paquete reflect permite crear proxies dinámicos en tiempo de ejecución para interfaces arbitrarias, recurso ampliamente utilizado por frameworks de inyección de dependencias, ORMs y librerías de seguridad.
Frameworks como Spring, CDI o Hibernate generan proxies para habilitar transacciones declarativas, carga perezosa o verificaciones de seguridad. Comprender el patrón ayuda a interpretar el comportamiento de estos contenedores y a configurar correctamente la serialización, el registro y la propagación de contexto.
Un abuso del patrón puede introducir capas innecesarias que degradan el rendimiento o hacen difícil seguir el flujo de llamadas. También es habitual duplicar lógica de negocio dentro del proxy, lo que genera inconsistencias con el objeto real.
Otro riesgo radica en no manejar adecuadamente los fallos del objeto real: si el apoderado no controla reintentos, tiempo de espera o la liberación de recursos, puede transformarse en un cuello de botella. Finalmente, es crucial definir claramente el contrato del Subject; si evoluciona sin mantener compatibilidad, clientes y proxies pueden quedar desincronizados.
Proxy comparte similitudes con Decorator, pero mientras Decorator agrega responsabilidades al objeto real, Proxy controla el acceso. Puede combinarse con Facade para exponer un punto de entrada simplificado y con Chain of Responsibility para encadenar verificaciones. Cuando se necesita adaptar la interfaz de un servicio heredado antes de delegar en el proxy, se recurre a Adapter. Asimismo, usar Proxy junto a Flyweight permite compartir instancias reales costosas y administrarlas desde un intermediario liviano.