🏗️ ¿Qué es el patrón Proxy?
El patrón Proxy es un patrón de diseño estructural que proporciona un sustituto o representante de otro objeto para controlar el acceso a él. En términos sencillos, un proxy actúa como un intermediario que se interpone entre el cliente y el objeto real (también llamado sujeto real o servicio), ofreciendo la misma interfaz pero añadiendo una capa de control.
La palabra «proxy» proviene del latín procurator, que significa «el que actúa en nombre de otro». En el mundo del software, esta metáfora es precisa: el objeto proxy recibe las peticiones del cliente, realiza algún procesamiento adicional (verificación de permisos, carga diferida, registro de accesos…) y delega la operación al objeto real cuando es apropiado.
A diferencia del patrón Bridge, que separa abstracción de implementación, el Proxy mantiene la misma abstracción pero controla cuándo, cómo y si se accede al objeto real. Y a diferencia del patrón Adapter, que adapta una interfaz a otra, el Proxy no cambia la interfaz: la replica exactamente.
📜 Contexto histórico: el Gang of Four
El patrón Proxy fue catalogado formalmente en 1994 por Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides — conocidos colectivamente como el Gang of Four (GoF) — en su influyente obra Design Patterns: Elements of Reusable Object-Oriented Software. Lo clasificaron dentro de los patrones estructurales, junto con Adapter, Bridge, Composite, Decorator, Facade y Flyweight.
Sin embargo, el concepto de intermediario en computación es mucho más antiguo. Los servidores proxy de red, que actúan como intermediarios entre un cliente y un servidor remoto, ya existían en la década de 1980. La idea de interponer un representante que controle el acceso a un recurso es un principio fundamental de la ingeniería de software que trasciende los lenguajes de programación.
Los GoF identificaron tres variantes principales: proxy remoto, proxy virtual y proxy de protección. Con el tiempo, la comunidad de desarrollo ha añadido variantes adicionales como el proxy de caché, el proxy de registro (logging proxy) y el proxy inteligente (smart proxy).
🎯 Problema que resuelve
Imaginemos que tenemos un objeto cuya creación o acceso es costoso en recursos. Podría tratarse de una imagen de alta resolución que ocupa megabytes en disco, una conexión a una base de datos remota, o un servicio que requiere autenticación. Sin el patrón Proxy, el cliente tendría que asumir directamente el coste de crear y gestionar ese objeto:
// Sin proxy: todas las imágenes se cargan al crear el documento
public class Editor {
private List<ImagenReal> imagenes = new ArrayList<>();
public void abrirDocumento(String ruta) {
// ⚠️ Carga TODAS las imágenes de disco inmediatamente
for (String archivo : obtenerRutasImagenes(ruta)) {
imagenes.add(new ImagenReal(archivo)); // Lectura costosa
}
}
}
🔹 Escenarios típicos donde el Proxy es la solución
| Escenario | Problema | Tipo de Proxy |
|---|---|---|
| Imágenes en un documento | Cargar todas al abrir es lento | Proxy Virtual |
| Objetos en servidor remoto | El cliente no accede directamente | Proxy Remoto |
| Recursos sensibles | No todos los usuarios deben acceder | Proxy de Protección |
| Consultas repetidas a BD | Repetir la misma consulta es ineficiente | Proxy de Caché |
| Auditoría de accesos | Registrar quién accede y cuándo | Proxy de Registro |
📐 Diagrama UML del patrón Proxy
La estructura del patrón Proxy se compone de tres participantes fundamentales, conectados mediante una interfaz común:
┌─────────────────────┐
│ <<interface>> │
│ Sujeto │
├─────────────────────┤
│ + operacion(): void │
└──────────┬──────────┘
│ implements
┌────────┴────────┐
│ │
┌─────────┴───────┐ ┌─────┴──────────────┐
│ SujetoReal │ │ Proxy │
├─────────────────┤ ├────────────────────┤
│ + operacion() │ │ - sujetoReal: Sujeto│
│ // lógica real│ │ + operacion(): void │
└─────────────────┘ └────────────────────┘
│ usa ──▶ SujetoReal
🔹 Participantes del patrón
| Participante | Responsabilidad |
|---|---|
| Sujeto (Subject) | Interfaz común que declara las operaciones. Permite la sustitución transparente. |
| SujetoReal (RealSubject) | El objeto real con la lógica de negocio. El proxy delega en él cuando es apropiado. |
| Proxy | Mantiene referencia al SujetoReal. Controla el acceso: creación bajo demanda, permisos, caché o registro. |
🗂️ Tipos de Proxy
El Gang of Four identificó tres variantes principales. Con la evolución del software, la comunidad ha consolidado variantes adicionales:
| Tipo | También llamado | Propósito principal |
|---|---|---|
| Virtual | Lazy Proxy | Retrasar la creación del objeto costoso hasta que se necesite |
| De Protección | Protection Proxy | Controlar el acceso según permisos o roles |
| Remoto | Remote Proxy, Stub | Representar localmente un objeto en otro espacio de direcciones |
| De Caché | Smart Proxy | Almacenar resultados para evitar operaciones repetidas |
| De Registro | Logging Proxy | Registrar operaciones para auditoría |
⏳ Proxy Virtual (Lazy Loading)
El Proxy Virtual es la variante más utilizada. Su propósito es retrasar la creación de un objeto costoso hasta el momento en que realmente se necesite (lazy initialization). El ejemplo clásico de los GoF es el editor de documentos con imágenes pesadas.
public interface Imagen {
void mostrar();
int getAncho();
int getAlto();
String getNombre();
}
import java.io.File;
public class ImagenReal implements Imagen {
private String rutaArchivo;
private byte[] datosPixeles;
private int ancho, alto;
public ImagenReal(String rutaArchivo) {
this.rutaArchivo = rutaArchivo;
cargarDesdeArchivo(); // ⚠️ Operación costosa
}
private void cargarDesdeArchivo() {
System.out.println(" [ImagenReal] Cargando: " + rutaArchivo);
try { Thread.sleep(500); } catch (InterruptedException e) { }
this.datosPixeles = new byte[1024 * 1024]; // 1 MB
this.ancho = 1920;
this.alto = 1080;
System.out.println(" [ImagenReal] Cargada: " + ancho + "x" + alto);
}
@Override public void mostrar() {
System.out.println(" [ImagenReal] Mostrando: " + rutaArchivo);
}
@Override public int getAncho() { return ancho; }
@Override public int getAlto() { return alto; }
@Override public String getNombre() {
return new File(rutaArchivo).getName();
}
}
public class ProxyImagen implements Imagen {
private String rutaArchivo;
private ImagenReal imagenReal; // null hasta que se necesite
public ProxyImagen(String rutaArchivo) {
this.rutaArchivo = rutaArchivo;
System.out.println(" [Proxy] Proxy creado para: " + rutaArchivo);
}
private void cargarSiNecesario() {
if (imagenReal == null) {
System.out.println(" [Proxy] Primera solicitud → cargando...");
imagenReal = new ImagenReal(rutaArchivo);
}
}
@Override public void mostrar() {
cargarSiNecesario();
imagenReal.mostrar();
}
@Override public int getAncho() {
cargarSiNecesario();
return imagenReal.getAncho();
}
@Override public int getAlto() {
cargarSiNecesario();
return imagenReal.getAlto();
}
@Override public String getNombre() {
return new java.io.File(rutaArchivo).getName();
}
}
public class DemoProxyVirtual {
public static void main(String[] args) {
System.out.println("=== Creando documento con 3 imágenes ===");
Imagen img1 = new ProxyImagen("/fotos/paisaje_4k.jpg");
Imagen img2 = new ProxyImagen("/fotos/retrato_hd.png");
Imagen img3 = new ProxyImagen("/fotos/diagrama_uml.svg");
System.out.println("\n=== Documento abierto (0 cargadas) ===");
System.out.println("Nombre img1: " + img1.getNombre());
System.out.println("\n=== Scroll hasta img2 ===");
img2.mostrar(); // Carga por primera vez
System.out.println("\n=== img2 de nuevo ===");
img2.mostrar(); // Ya cargada — no recarga
}
}
=== Creando documento con 3 imágenes === [Proxy] Proxy creado para: /fotos/paisaje_4k.jpg [Proxy] Proxy creado para: /fotos/retrato_hd.png [Proxy] Proxy creado para: /fotos/diagrama_uml.svg === Documento abierto (0 cargadas) === Nombre img1: paisaje_4k.jpg === Scroll hasta img2 === [Proxy] Primera solicitud → cargando... [ImagenReal] Cargando: /fotos/retrato_hd.png [ImagenReal] Cargada: 1920x1080 [ImagenReal] Mostrando: /fotos/retrato_hd.png === img2 de nuevo === [ImagenReal] Mostrando: /fotos/retrato_hd.png
ProxyImagen (ligeros). La imagen real se carga en la primera llamada a mostrar(). La segunda llamada reutiliza la instancia ya cargada.
🔒 Proxy de Protección (acceso controlado)
El Proxy de Protección controla el acceso al objeto real en función de los permisos del solicitante. El proxy verifica las credenciales antes de delegar la operación al sujeto real.
public interface Documento {
String leer();
void escribir(String contenido);
void eliminar();
}
public class DocumentoConfidencial implements Documento {
private String contenido;
private final String titulo;
public DocumentoConfidencial(String titulo, String contenido) {
this.titulo = titulo;
this.contenido = contenido;
}
@Override public String leer() { return contenido; }
@Override public void escribir(String nuevoContenido) {
this.contenido = nuevoContenido;
System.out.println("[Doc] Actualizado: " + titulo);
}
@Override public void eliminar() {
this.contenido = null;
System.out.println("[Doc] Eliminado: " + titulo);
}
}
public class ProxyDocumentoProtegido implements Documento {
private final DocumentoConfidencial docReal;
private final String rolUsuario;
public ProxyDocumentoProtegido(DocumentoConfidencial doc, String rol) {
this.docReal = doc;
this.rolUsuario = rol;
}
@Override public String leer() {
if ("INVITADO".equals(rolUsuario)) {
throw new SecurityException("Acceso denegado: invitados no pueden leer");
}
System.out.println("[Proxy] Lectura concedida a: " + rolUsuario);
return docReal.leer();
}
@Override public void escribir(String contenido) {
if (!"ADMIN".equals(rolUsuario) && !"EDITOR".equals(rolUsuario)) {
throw new SecurityException("Rol " + rolUsuario + " no puede escribir");
}
System.out.println("[Proxy] Escritura concedida a: " + rolUsuario);
docReal.escribir(contenido);
}
@Override public void eliminar() {
if (!"ADMIN".equals(rolUsuario)) {
throw new SecurityException("Solo ADMIN puede eliminar");
}
System.out.println("[Proxy] Eliminación concedida a: " + rolUsuario);
docReal.eliminar();
}
}
🌐 Proxy Remoto
El Proxy Remoto proporciona una representación local de un objeto que reside en otro espacio de direcciones: otra JVM, otro servidor o incluso otro continente. En Java, este concepto es la base de RMI (Remote Method Invocation) y Enterprise JavaBeans (EJB). El stub generado por RMI es precisamente un proxy remoto.
public interface ServicioClima {
String obtenerTemperatura(String ciudad);
}
public class ServicioClimaReal implements ServicioClima {
@Override
public String obtenerTemperatura(String ciudad) {
return "22°C en " + ciudad;
}
}
public class ProxyServicioClima implements ServicioClima {
private final String urlServidor;
public ProxyServicioClima(String urlServidor) {
this.urlServidor = urlServidor;
}
@Override
public String obtenerTemperatura(String ciudad) {
System.out.println("[ProxyRemoto] Petición a: " + urlServidor);
// Aquí iría HttpClient, JSON, manejo de errores...
try { Thread.sleep(200); } catch (InterruptedException e) { }
return "22°C en " + ciudad + " (vía proxy remoto)";
}
}
💾 Proxy de Caché (Smart Proxy)
El Proxy de Caché almacena resultados de operaciones previas para evitar repetirlas. Es útil cuando el objeto real realiza operaciones costosas cuyos resultados no cambian frecuentemente.
import java.util.HashMap;
import java.util.Map;
public interface RepositorioProductos {
String buscarPorId(int id);
}
public class RepositorioProductosBD implements RepositorioProductos {
@Override
public String buscarPorId(int id) {
System.out.println(" [BD] SELECT * FROM productos WHERE id=" + id);
try { Thread.sleep(300); } catch (InterruptedException e) { }
return "Producto #" + id + " (desde BD)";
}
}
public class ProxyCacheProductos implements RepositorioProductos {
private final RepositorioProductosBD repoReal;
private final Map<Integer, String> cache = new HashMap<>();
public ProxyCacheProductos(RepositorioProductosBD repo) {
this.repoReal = repo;
}
@Override
public String buscarPorId(int id) {
if (cache.containsKey(id)) {
System.out.println(" [Cache] HIT para id=" + id);
return cache.get(id);
}
System.out.println(" [Cache] MISS → delegando a BD id=" + id);
String resultado = repoReal.buscarPorId(id);
cache.put(id, resultado);
return resultado;
}
public void invalidar(int id) { cache.remove(id); }
public void invalidarTodo() { cache.clear(); }
}
🔀 Proxy vs. otros patrones estructurales
El patrón Proxy se confunde con otros patrones que «envuelven» un objeto. La diferencia está en la intención:
| Patrón | Intención | ¿Cambia interfaz? | ¿Controla acceso? |
|---|---|---|---|
| Proxy | Controlar el acceso | ❌ No | ✅ Sí |
| Decorator | Añadir funcionalidad | ❌ No | ❌ No |
| Adapter | Convertir interfaces | ✅ Sí | ❌ No |
| Bridge | Separar abstracción/implementación | ✅ Sí | ❌ No |
| Facade | Simplificar interfaz compleja | ✅ Sí | ❌ No |
🧩 Ejemplo integrador: sistema de documentos
Este ejemplo combina Proxy Virtual con Proxy de Registro en un sistema de gestión documental, demostrando cómo los proxies pueden encadenarse:
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
// ── Interfaz común ──
public interface Recurso {
String obtenerContenido();
String getNombre();
long getTamano();
}
// ── Sujeto Real ──
public class ArchivoGrande implements Recurso {
private final String nombre;
private final byte[] datos;
public ArchivoGrande(String nombre) {
this.nombre = nombre;
System.out.println(" [Archivo] Cargando '" + nombre + "'...");
try { Thread.sleep(800); } catch (InterruptedException e) { }
this.datos = new byte[5 * 1024 * 1024]; // 5 MB
System.out.println(" [Archivo] Cargado (" + getTamano()/1024 + " KB)");
}
@Override public String obtenerContenido() {
return "Contenido de " + nombre + " (" + datos.length + " bytes)";
}
@Override public String getNombre() { return nombre; }
@Override public long getTamano() { return datos.length; }
}
// ── Proxy Virtual ──
public class ProxyVirtualRecurso implements Recurso {
private final String nombre;
private ArchivoGrande archivoReal;
public ProxyVirtualRecurso(String nombre) { this.nombre = nombre; }
private void cargarSiNecesario() {
if (archivoReal == null) archivoReal = new ArchivoGrande(nombre);
}
@Override public String obtenerContenido() {
cargarSiNecesario();
return archivoReal.obtenerContenido();
}
@Override public String getNombre() { return nombre; }
@Override public long getTamano() {
cargarSiNecesario();
return archivoReal.getTamano();
}
}
// ── Proxy de Registro ──
public class ProxyRegistroRecurso implements Recurso {
private final Recurso recursoInterno;
private final List<String> log = new ArrayList<>();
private static final DateTimeFormatter FMT =
DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
public ProxyRegistroRecurso(Recurso recurso) {
this.recursoInterno = recurso;
}
@Override public String obtenerContenido() {
String e = FMT.format(LocalDateTime.now()) + " → obtenerContenido()";
log.add(e);
System.out.println(" [Log] " + e);
return recursoInterno.obtenerContenido();
}
@Override public String getNombre() { return recursoInterno.getNombre(); }
@Override public long getTamano() { return recursoInterno.getTamano(); }
public List<String> getRegistro() { return List.copyOf(log); }
}
// ── Demo ──
public class DemoSistemaDocumental {
public static void main(String[] args) {
// Encadenamiento: Log → Virtual → ArchivoGrande
var informe = new ProxyRegistroRecurso(
new ProxyVirtualRecurso("informe_2025.pdf"));
var presentacion = new ProxyRegistroRecurso(
new ProxyVirtualRecurso("presentacion_q4.pptx"));
System.out.println("1. Documentos registrados (no cargados)");
System.out.println(" - " + informe.getNombre());
System.out.println(" - " + presentacion.getNombre());
System.out.println("\n2. Abriendo informe:");
System.out.println(" " + informe.obtenerContenido());
System.out.println("\n3. Informe de nuevo (ya cargado):");
System.out.println(" " + informe.obtenerContenido());
System.out.println("\n4. Presentación nunca se cargó ✅");
System.out.println("\n5. Log de accesos:");
informe.getRegistro().forEach(e -> System.out.println(" • " + e));
}
}
ProxyRegistroRecurso envuelve a ProxyVirtualRecurso, que controla la creación de ArchivoGrande. Cada proxy tiene una responsabilidad clara (SRP).
✅ Buenas prácticas y errores frecuentes
🔹 Buenas prácticas
| Práctica | Motivo |
|---|---|
| Usar interfaces, no clases concretas | Permite sustituir proxy ↔ real sin cambiar el cliente |
| Un proxy = una responsabilidad | Evita proxies «omnibus» que cachean, validan y logean a la vez |
Considerar java.lang.reflect.Proxy | Para proxies dinámicos sin código repetitivo |
| Documentar que se trabaja con proxy | Facilita mantenimiento y depuración |
| Gestionar concurrencia en caché | Usar ConcurrentHashMap en entornos multihilo |
🔹 Errores frecuentes
| Error | Consecuencia | Solución |
|---|---|---|
| No implementar toda la interfaz | Violación del contrato | Implementar todos los métodos |
| Proxy virtual sin null-check | NullPointerException | Manejar excepciones en cargarSiNecesario() |
| Caché sin invalidación | Datos obsoletos | Definir TTL o invalidación por eventos |
| Confundir Proxy con Decorator | Diseño con intención incorrecta | Preguntarse: ¿controlo acceso o añado funcionalidad? |
📝 Ejercicios prácticos
Ejercicio 1 — Proxy Virtual para conexión a BD
Crea una interfaz ConexionBD con métodos conectar(), ejecutarConsulta(String sql) y cerrar(). Implementa ConexionBDReal que simule 2 segundos de conexión. Luego implementa ProxyConexionBD que retrase la conexión hasta la primera consulta.
Ver solución
public interface ConexionBD {
void conectar();
String ejecutarConsulta(String sql);
void cerrar();
}
public class ConexionBDReal implements ConexionBD {
private boolean conectada = false;
@Override public void conectar() {
System.out.println("[BD] Conectando...");
try { Thread.sleep(2000); } catch (InterruptedException e) { }
conectada = true;
System.out.println("[BD] Conectada");
}
@Override public String ejecutarConsulta(String sql) {
if (!conectada) throw new IllegalStateException("No conectada");
return "Resultado de: " + sql;
}
@Override public void cerrar() {
conectada = false;
System.out.println("[BD] Cerrada");
}
}
public class ProxyConexionBD implements ConexionBD {
private ConexionBDReal conexionReal;
private void asegurarConexion() {
if (conexionReal == null) {
conexionReal = new ConexionBDReal();
conexionReal.conectar();
}
}
@Override public void conectar() {
System.out.println("[Proxy] Registrada (se conectará al primer uso)");
}
@Override public String ejecutarConsulta(String sql) {
asegurarConexion();
return conexionReal.ejecutarConsulta(sql);
}
@Override public void cerrar() {
if (conexionReal != null) { conexionReal.cerrar(); conexionReal = null; }
}
}
Ejercicio 2 — Proxy de Protección con roles
Diseña una interfaz CuentaBancaria con consultarSaldo(), depositar(double) y retirar(double). Implementa un proxy que permita a todos consultar saldo, pero solo al «TITULAR» depositar y retirar, y al «AUTORIZADO» solo depositar.
Ver solución
public interface CuentaBancaria {
double consultarSaldo();
void depositar(double cantidad);
void retirar(double cantidad);
}
public class CuentaReal implements CuentaBancaria {
private double saldo;
public CuentaReal(double saldoInicial) { this.saldo = saldoInicial; }
@Override public double consultarSaldo() { return saldo; }
@Override public void depositar(double c) {
saldo += c;
System.out.println("Depósito: +" + c + " → " + saldo);
}
@Override public void retirar(double c) {
if (c > saldo) throw new IllegalArgumentException("Fondos insuficientes");
saldo -= c;
System.out.println("Retiro: -" + c + " → " + saldo);
}
}
public class ProxyCuentaProtegida implements CuentaBancaria {
private final CuentaReal cuentaReal;
private final String rol;
public ProxyCuentaProtegida(CuentaReal cuenta, String rol) {
this.cuentaReal = cuenta; this.rol = rol;
}
@Override public double consultarSaldo() {
return cuentaReal.consultarSaldo();
}
@Override public void depositar(double c) {
if (!"TITULAR".equals(rol) && !"AUTORIZADO".equals(rol))
throw new SecurityException("Rol " + rol + " no puede depositar");
cuentaReal.depositar(c);
}
@Override public void retirar(double c) {
if (!"TITULAR".equals(rol))
throw new SecurityException("Solo TITULAR puede retirar");
cuentaReal.retirar(c);
}
}
Ejercicio 3 — Proxy de Caché con TTL
Extiende ProxyCacheProductos para que cada entrada tenga un TTL de 5 segundos. Pasado ese tiempo, la siguiente consulta va a BD y actualiza la caché.
Ver solución
import java.util.HashMap;
import java.util.Map;
public class ProxyCacheTTL implements RepositorioProductos {
private final RepositorioProductosBD repoReal;
private final Map<Integer, String> cache = new HashMap<>();
private final Map<Integer, Long> timestamps = new HashMap<>();
private final long ttlMs;
public ProxyCacheTTL(RepositorioProductosBD repo, long ttlMs) {
this.repoReal = repo; this.ttlMs = ttlMs;
}
@Override public String buscarPorId(int id) {
long ahora = System.currentTimeMillis();
if (cache.containsKey(id)) {
long edad = ahora - timestamps.get(id);
if (edad < ttlMs) {
System.out.println("[Cache] HIT (edad:" + edad + "ms) id=" + id);
return cache.get(id);
}
System.out.println("[Cache] EXPIRADO id=" + id);
cache.remove(id); timestamps.remove(id);
}
System.out.println("[Cache] MISS → BD id=" + id);
String r = repoReal.buscarPorId(id);
cache.put(id, r); timestamps.put(id, ahora);
return r;
}
}
// Uso: new ProxyCacheTTL(repoBD, 5000) → TTL 5 segundos
❓ Preguntas frecuentes sobre El Patrón Proxy en Java
Las dudas más comunes respondidas de forma clara y directa.
💬 Foro de discusión
¿Tienes dudas sobre El Patrón Proxy en Java? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!