Patrón Proxy
Un objeto con la misma interfaz que otro que controla el acceso a él: lo crea solo cuando hace falta (perezoso), guarda sus respuestas (caché) o comprueba permisos antes de dejar pasar cada llamada.
nivel intermedioTambién: apoderado, sustituto, carga perezosa, lazy loading
Visualízalo paso a paso
Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.
Proxy
Escribe lo que hace la galería con sus tres fotos, separado por comas: nombre 1 (pide el nombre) o ver 2 (abre la foto).
Paso 1
La galería tiene sus tres fotos desde el principio, pero son proxies: ninguna imagen se ha cargado todavía (real = null).
1class ImagenProxy implements Imagen {
2 private ImagenReal real; // fotos cargadas = 0 de 3, memoria = 0 MB
3
4 public String nombre() { return nombre; }
5
6 public void mostrar() {
7 if (real == null)
8 real = new ImagenReal(nombre);
9 real.mostrar();
10 }
11}Variables
- fotos cargadas
- 0 de 3
- memoria
- 0 MB
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
Hay objetos caros de crear (una imagen de 2 MB, una entidad que exige una consulta a la base de datos), servicios lentos a los que se pregunta lo mismo muchas veces u operaciones que solo pueden hacer algunos usuarios. En todos los casos, alguien debería ponerse delante del objeto real y decidir cuándo y cómo se le llama.
El patrón Proxy pone delante del objeto real otro con su misma interfaz. El cliente no nota la diferencia: llama a mostrar() o a prevision() como siempre. El proxy decide: crear el objeto real la primera vez que se usa (proxy virtual), devolver una respuesta guardada (proxy de caché), comprobar permisos (proxy de protección) o hacer la llamada por la red (proxy remoto).
Se parece al Decorator (los dos envuelven a un objeto con su misma interfaz), pero la intención es otra: el decorador añade funciones y se apila, el proxy controla el acceso y normalmente es él quien crea o gestiona el objeto real. Hibernate y Spring crean proxies sin que los veas: las entidades perezosas, @Transactional o @Cacheable.
Cuándo usarlo
- Crear un objeto es caro y quizá no se llegue a usar: se crea en la primera llamada (imágenes, entidades, conexiones).
- Se repiten llamadas lentas con los mismos argumentos: el proxy guarda las respuestas.
- Hay que comprobar permisos, registrar accesos o limitar las llamadas sin tocar la clase real.
- El objeto real está en otro proceso u otra máquina y se quiere usar como si fuera local (RMI, clientes de servicios web).
Cuándo no
- Si el objeto es barato de crear y de usar: el proxy solo añade una capa.
- Si las respuestas cambian a menudo y una caché podría devolver datos viejos sin una forma clara de invalidarla.
Participantes
- Sujeto. La interfaz común (
Imagen) que usa el cliente. - Sujeto real. La clase que hace el trabajo (
ImagenReal), cara de crear o de llamar. - Proxy. Implementa la misma interfaz, guarda una referencia al sujeto real (que puede ser
nullhasta que haga falta) y controla cada llamada antes de delegar. - Cliente. Usa la interfaz y no sabe si habla con el proxy o con el objeto real.
Diagrama de clases
Ver el diagrama en PlantUML
1@startuml
2interface Imagen {
3 +nombre() : String
4 +mostrar() : void
5}
6class ImagenReal {
7 -nombre : String
8 +ImagenReal(nombre : String)
9}
10class ImagenProxy {
11 -nombre : String
12 -real : ImagenReal
13}
14class Galeria
15Imagen <|.. ImagenReal
16Imagen <|.. ImagenProxy
17ImagenProxy --> "0..1" ImagenReal : real
18Galeria o-- "0..*" Imagen : fotos
19@endumlPuedes copiarlo en el editor de diagramas UML y modificarlo.
El código
Una galería que carga las fotos al abrirlas
Las tres fotos están en la galería desde el principio, pero solo se carga del disco la que se abre, y solo una vez.
1import java.util.*;
2
3interface Imagen {
4 String nombre();
5 void mostrar();
6}
7
8/** El objeto real: crearlo es caro (lee el fichero entero en el constructor). */
9class ImagenReal implements Imagen {
10 private final String nombre;
11
12 ImagenReal(String nombre) {
13 this.nombre = nombre;
14 System.out.println(" (cargando " + nombre + " desde el disco: 2 MB)");
15 }
16
17 public String nombre() { return nombre; }
18 public void mostrar() { System.out.println(" Mostrando " + nombre); }
19}
20
21/** El proxy: la misma interfaz, y no carga la imagen hasta que alguien la quiere ver. */
22class ImagenProxy implements Imagen {
23 private final String nombre;
24 private ImagenReal real; // null hasta que haga falta
25
26 ImagenProxy(String nombre) { this.nombre = nombre; }
27
28 public String nombre() { return nombre; } // esto lo sabe sin cargar nada
29
30 public void mostrar() {
31 if (real == null) real = new ImagenReal(nombre); // carga perezosa: solo la primera vez
32 real.mostrar();
33 }
34}
35
36public class Main {
37 public static void main(String[] args) {
38 List<Imagen> galeria = List.of(new ImagenProxy("playa.jpg"), new ImagenProxy("montaña.jpg"), new ImagenProxy("ciudad.jpg"));
39 List<String> nombres = new ArrayList<>();
40 for (Imagen i : galeria) nombres.add(i.nombre());
41 System.out.println("Galería con " + galeria.size() + " fotos: " + String.join(", ", nombres));
42 System.out.println("Se abre la segunda foto:");
43 galeria.get(1).mostrar();
44 System.out.println("Se vuelve a abrir:");
45 galeria.get(1).mostrar();
46 }
47}from abc import ABC, abstractmethod
class Imagen(ABC):
@abstractmethod
def nombre(self): ...
@abstractmethod
def mostrar(self): ...
class ImagenReal(Imagen):
"""El objeto real: crearlo es caro (lee el fichero entero en el constructor)."""
def __init__(self, nombre):
self._nombre = nombre
print(f" (cargando {nombre} desde el disco: 2 MB)")
def nombre(self):
return self._nombre
def mostrar(self):
print(f" Mostrando {self._nombre}")
class ImagenProxy(Imagen):
"""El proxy: la misma interfaz, y no carga la imagen hasta que alguien la quiere ver."""
def __init__(self, nombre):
self._nombre = nombre
self._real = None # None hasta que haga falta
def nombre(self):
return self._nombre # esto lo sabe sin cargar nada
def mostrar(self):
if self._real is None:
self._real = ImagenReal(self._nombre) # carga perezosa: solo la primera vez
self._real.mostrar()
galeria = [ImagenProxy("playa.jpg"), ImagenProxy("montaña.jpg"), ImagenProxy("ciudad.jpg")]
print(f"Galería con {len(galeria)} fotos: {', '.join(i.nombre() for i in galeria)}")
print("Se abre la segunda foto:")
galeria[1].mostrar()
print("Se vuelve a abrir:")
galeria[1].mostrar()/** El objeto real: crearlo es caro (lee el fichero entero en el constructor). */
class ImagenReal {
constructor(nombre) {
this._nombre = nombre;
console.log(` (cargando ${nombre} desde el disco: 2 MB)`);
}
nombre() { return this._nombre; }
mostrar() { console.log(` Mostrando ${this._nombre}`); }
}
/** El proxy: los mismos métodos, y no carga la imagen hasta que alguien la quiere ver. */
class ImagenProxy {
#real = null; // null hasta que haga falta
constructor(nombre) { this._nombre = nombre; }
nombre() { return this._nombre; } // esto lo sabe sin cargar nada
mostrar() {
this.#real ??= new ImagenReal(this._nombre); // carga perezosa: solo la primera vez
this.#real.mostrar();
}
}
const galeria = [new ImagenProxy("playa.jpg"), new ImagenProxy("montaña.jpg"), new ImagenProxy("ciudad.jpg")];
console.log(`Galería con ${galeria.length} fotos: ${galeria.map((i) => i.nombre()).join(", ")}`);
console.log("Se abre la segunda foto:");
galeria[1].mostrar();
console.log("Se vuelve a abrir:");
galeria[1].mostrar();using System;
using System.Collections.Generic;
using System.Linq;
interface IImagen {
string Nombre { get; }
void Mostrar();
}
// El objeto real: crearlo es caro (lee el fichero entero en el constructor).
class ImagenReal : IImagen {
public string Nombre { get; }
public ImagenReal(string nombre) {
Nombre = nombre;
Console.WriteLine(quot; (cargando {nombre} desde el disco: 2 MB)");
}
public void Mostrar() => Console.WriteLine(quot; Mostrando {Nombre}");
}
// El proxy: la misma interfaz, y no carga la imagen hasta que alguien la quiere ver.
class ImagenProxy : IImagen {
private ImagenReal? real; // null hasta que haga falta
public string Nombre { get; } // esto lo sabe sin cargar nada
public ImagenProxy(string nombre) => Nombre = nombre;
public void Mostrar() {
real ??= new ImagenReal(Nombre); // carga perezosa: solo la primera vez
real.Mostrar();
}
}
class Program {
static void Main() {
var galeria = new List<IImagen> { new ImagenProxy("playa.jpg"), new ImagenProxy("montaña.jpg"), new ImagenProxy("ciudad.jpg") };
Console.WriteLine(quot;Galería con {galeria.Count} fotos: {string.Join(", ", galeria.Select(i => i.Nombre))}");
Console.WriteLine("Se abre la segunda foto:");
galeria[1].Mostrar();
Console.WriteLine("Se vuelve a abrir:");
galeria[1].Mostrar();
}
}<?php
interface Imagen {
public function nombre(): string;
public function mostrar(): void;
}
/** El objeto real: crearlo es caro (lee el fichero entero en el constructor). */
class ImagenReal implements Imagen {
public function __construct(private string $nombre) {
echo " (cargando $nombre desde el disco: 2 MB)\n";
}
public function nombre(): string { return $this->nombre; }
public function mostrar(): void { echo " Mostrando {$this->nombre}\n"; }
}
/** El proxy: la misma interfaz, y no carga la imagen hasta que alguien la quiere ver. */
class ImagenProxy implements Imagen {
private ?ImagenReal $real = null; // null hasta que haga falta
public function __construct(private string $nombre) {}
public function nombre(): string { return $this->nombre; } // esto lo sabe sin cargar nada
public function mostrar(): void {
$this->real ??= new ImagenReal($this->nombre); // carga perezosa: solo la primera vez
$this->real->mostrar();
}
}
$galeria = [new ImagenProxy("playa.jpg"), new ImagenProxy("montaña.jpg"), new ImagenProxy("ciudad.jpg")];
echo "Galería con ", count($galeria), " fotos: ", implode(", ", array_map(fn (Imagen $i) => $i->nombre(), $galeria)), "\n";
echo "Se abre la segunda foto:\n";
$galeria[1]->mostrar();
echo "Se vuelve a abrir:\n";
$galeria[1]->mostrar();Salida al ejecutarlo (la misma en los 5 lenguajes)
Galería con 3 fotos: playa.jpg, montaña.jpg, ciudad.jpg Se abre la segunda foto: (cargando montaña.jpg desde el disco: 2 MB) Mostrando montaña.jpg Se vuelve a abrir: Mostrando montaña.jpg
Los proxies que crean los frameworks
Hibernate y Spring envuelven tus objetos en proxies para cargar datos al usarlos o para añadir transacciones y cachés.
1// Hibernate/JPA: getReference devuelve un proxy de Alumno sin consultar la base de datos;
2// la consulta se hace la primera vez que se lee un atributo (carga perezosa).
3Alumno a = entityManager.getReference(Alumno.class, 7L);
4System.out.println(a.getNombre()); // aquí se lanza el SELECT
5
6// Spring crea proxies para añadir comportamiento alrededor de tus métodos: la transacción,
7// la caché o la seguridad se ejecutan en el proxy, antes y después de llamar a tu clase.
8@Service
9class MatriculaService {
10 @Transactional // el proxy abre y cierra la transacción
11 public void matricular(Alumno a, Ciclo c) { /* ... */ }
12
13 @Cacheable("ciclos") // el proxy devuelve lo guardado si ya se pidió
14 public List<Ciclo> ciclos() { /* ... */ }
15}
16
17// Y a mano, con java.lang.reflect.Proxy se crea un proxy de cualquier interfaz en tiempo de ejecución.class ProxyRegistro:
"""Un proxy genérico: delega cualquier atributo en el objeto real y registra cada llamada."""
def __init__(self, real):
self._real = real
def __getattr__(self, nombre): # solo se llama con lo que el proxy no tiene
atributo = getattr(self._real, nombre)
if callable(atributo):
def envoltura(*args, **kwargs):
print(f"Llamada a {nombre}{args}")
return atributo(*args, **kwargs)
return envoltura
return atributo
# Django y SQLAlchemy usan objetos perezosos parecidos: la consulta no se lanza hasta que se usan los datos.// JavaScript trae los proxies de serie: new Proxy(objeto, manejador) intercepta cada acceso
const alumno = { nombre: "Ana", nota: 8 };
const vigilado = new Proxy(alumno, {
get(obj, prop) {
console.log(`Lectura de ${String(prop)}`);
return obj[prop];
},
set(obj, prop, valor) {
if (prop === "nota" && (valor < 0 || valor > 10)) throw new RangeError("Nota fuera de rango");
obj[prop] = valor;
return true;
},
});
vigilado.nota = 9; // pasa por el proxy, que comprueba el valor
// Vue usa Proxy para saber qué datos cambian y volver a pintar solo lo necesario.// Lazy<T> es un proxy virtual de la biblioteca: crea el objeto la primera vez que se pide Value
private readonly Lazy<Informe> informe = new(() => new Informe("ventas-2026.xlsx"));
public void Ver() => informe.Value.Mostrar(); // aquí se crea, y solo una vez
// Entity Framework crea proxies de tus entidades para cargar las relaciones al usarlas
// (UseLazyLoadingProxies), y DispatchProxy permite crear proxies de cualquier interfaz.<?php
// __call recibe las llamadas a métodos que la clase no tiene: base de muchos proxies en PHP
class ProxyRegistro {
public function __construct(private object $real) {}
public function __call(string $metodo, array $args): mixed {
echo "Llamada a $metodo\n";
return $this->real->$metodo(...$args);
}
}
// Doctrine (el ORM de Symfony) genera clases proxy de tus entidades para cargarlas al usarlas.En la práctica
- Las relaciones perezosas de Hibernate y JPA (
FetchType.LAZY,getReference): el objeto relacionado es un proxy que lanza la consulta al usarse. - Spring:
@Transactional,@Cacheable,@Asyncy la seguridad de métodos funcionan con proxies alrededor de tus beans. - Los clientes de servicios web y RMI: un objeto local que convierte cada llamada en una petición por la red.
- En JavaScript,
new Proxy(objeto, manejador)intercepta cualquier acceso; Vue lo usa para saber qué datos cambian.
Errores típicos
- Que el proxy no implemente la misma interfaz que el objeto real: el cliente ya no puede usarlos indistintamente.
- Crear el objeto real en el constructor del proxy: se pierde la carga perezosa.
- Una caché sin forma de invalidarla: devuelve datos viejos para siempre.
- Dejar accesible el objeto real sin proxy: cualquier parte del programa se salta la comprobación de permisos.
Ejercicios
Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.
1. Caché de un servicio del tiempo
Cada consulta al servicio del tiempo va por la red. El programa usa TiempoConCache, que de momento solo pasa cada llamada al servicio real. Conviértelo en un proxy de caché: que guarde las respuestas y solo pregunte al servidor lo que no sabe, y que olvidar borre lo guardado.
- Órdenes:
prevision ciudad,olvidar ciudadyolvidar todo. El servidor ya escribe(consulta al servidor: ciudad)cada vez que se le llama. - Al final:
Consultas al servidor: n · guardadas: m, donde m es cuántas respuestas tiene guardadas la caché. Las ciudades sin datos también se guardan.
Ejemplo
prevision Madrid prevision Bilbao prevision Madrid prevision Madrid prevision Bilbao
(consulta al servidor: Madrid) Madrid: soleado, 24 °C (consulta al servidor: Bilbao) Bilbao: lluvia, 17 °C Madrid: soleado, 24 °C Madrid: soleado, 24 °C Bilbao: lluvia, 17 °C Consultas al servidor: 2 · guardadas: 2
Ver la solución explicada
1import java.util.*;
2
3interface Tiempo {
4 String prevision(String ciudad);
5}
6
7/** El servicio real: cada consulta va por la red y tarda (aquí, la escribe y la cuenta). */
8class ServicioRemoto implements Tiempo {
9 static int consultas = 0;
10 private static final Map<String, String> DATOS = Map.of(
11 "Madrid", "soleado, 24 °C", "Bilbao", "lluvia, 17 °C", "Sevilla", "soleado, 31 °C", "Vigo", "nublado, 19 °C");
12
13 public String prevision(String ciudad) {
14 consultas++;
15 System.out.println(" (consulta al servidor: " + ciudad + ")");
16 return DATOS.getOrDefault(ciudad, "sin datos");
17 }
18}
19
20/** El proxy: guarda las respuestas y solo pregunta al servicio real lo que no sabe. */
21class TiempoConCache implements Tiempo {
22 private final Tiempo real;
23 private final Map<String, String> cache = new HashMap<>();
24
25 TiempoConCache(Tiempo real) { this.real = real; }
26
27 public String prevision(String ciudad) {
28 return cache.computeIfAbsent(ciudad, real::prevision);
29 }
30
31 void olvidar(String ciudad) { cache.remove(ciudad); }
32 void olvidarTodo() { cache.clear(); }
33 int guardadas() { return cache.size(); }
34}
35
36public class Main {
37 public static void main(String[] args) {
38 TiempoConCache tiempo = new TiempoConCache(new ServicioRemoto());
39 Scanner sc = new Scanner(System.in);
40 while (sc.hasNextLine()) {
41 String[] p = sc.nextLine().trim().split("\\s+");
42 if (p[0].isEmpty()) continue;
43 if (p[0].equals("prevision") && p.length == 2) System.out.println(p[1] + ": " + tiempo.prevision(p[1]));
44 else if (p[0].equals("olvidar") && p.length == 2) {
45 if (p[1].equals("todo")) tiempo.olvidarTodo();
46 else tiempo.olvidar(p[1]);
47 System.out.println("Olvidado: " + p[1]);
48 } else System.out.println("Orden no válida: " + String.join(" ", p));
49 }
50 System.out.println("Consultas al servidor: " + ServicioRemoto.consultas + " · guardadas: " + tiempo.guardadas());
51 }
52}El proxy tiene la misma interfaz que el servicio: se puede poner o quitar sin tocar el resto del programa.
Invalidar la caché es parte del diseño: si los datos cambian, hay que poder olvidar lo guardado.
2. Documentos con permisos
Todos los usuarios acceden a los documentos a través de un ProxySeguridad, que de momento deja hacer cualquier cosa. Escríbelo para que, antes de cada operación, compruebe que el rol del usuario la permite y, si no, lance una SecurityException con el mensaje pedido sin llegar a tocar el almacén.
- Órdenes:
usuario nombre rol,leer doc,escribir doc textoyborrar doc. Al final,Intentos denegados: n · documentos: lista(elmaincuenta las excepciones). - Roles:
lectorpuede leer;editor, leer y escribir;admin, todo. Un rol que no existe:IllegalArgumentExceptionconRol desconocido: rol(al crear el proxy). - Mensaje:
Acceso denegado: nombre (rol) no puede acción doc.
Ejemplo
usuario ana lector leer acta escribir acta Cambio usuario luis editor escribir acta Reunión del martes leer acta borrar notas usuario eva admin borrar notas escribir horario Lunes a viernes
Sesión de ana (lector) acta: Reunión del lunes Acceso denegado: ana (lector) no puede escribir acta Sesión de luis (editor) Guardado: acta acta: Reunión del martes Acceso denegado: luis (editor) no puede borrar notas Sesión de eva (admin) Borrado: notas Guardado: horario Intentos denegados: 2 · documentos: acta, horario
Ver la solución explicada
1import java.util.*;
2
3interface Documentos {
4 String leer(String doc);
5 void escribir(String doc, String texto);
6 void borrar(String doc);
7}
8
9/** El almacén real: no comprueba permisos. */
10class Almacen implements Documentos {
11 private final Map<String, String> docs = new TreeMap<>(Map.of("acta", "Reunión del lunes", "notas", "Primera evaluación"));
12 public String leer(String doc) { return docs.getOrDefault(doc, "(no existe)"); }
13 public void escribir(String doc, String texto) { docs.put(doc, texto); }
14 public void borrar(String doc) { docs.remove(doc); }
15 Set<String> nombres() { return docs.keySet(); }
16}
17
18/** El proxy de protección: la misma interfaz, y antes de cada operación comprueba el rol. */
19class ProxySeguridad implements Documentos {
20 private static final Map<String, Set<String>> PERMISOS = Map.of(
21 "lector", Set.of("leer"),
22 "editor", Set.of("leer", "escribir"),
23 "admin", Set.of("leer", "escribir", "borrar"));
24
25 private final String usuario, rol;
26 private final Documentos real;
27
28 ProxySeguridad(String usuario, String rol, Documentos real) {
29 if (!PERMISOS.containsKey(rol)) throw new IllegalArgumentException("Rol desconocido: " + rol);
30 this.usuario = usuario;
31 this.rol = rol;
32 this.real = real;
33 }
34
35 private void comprobar(String accion, String doc) {
36 if (!PERMISOS.get(rol).contains(accion))
37 throw new SecurityException("Acceso denegado: " + usuario + " (" + rol + ") no puede " + accion + " " + doc);
38 }
39
40 public String leer(String doc) {
41 comprobar("leer", doc);
42 return real.leer(doc);
43 }
44
45 public void escribir(String doc, String texto) {
46 comprobar("escribir", doc);
47 real.escribir(doc, texto);
48 }
49
50 public void borrar(String doc) {
51 comprobar("borrar", doc);
52 real.borrar(doc);
53 }
54}
55
56public class Main {
57 public static void main(String[] args) {
58 Almacen almacen = new Almacen();
59 Documentos docs = null;
60 int denegados = 0;
61 Scanner sc = new Scanner(System.in);
62 while (sc.hasNextLine()) {
63 String linea = sc.nextLine().trim();
64 if (linea.isEmpty()) continue;
65 String[] p = linea.split("\\s+", 3);
66 try {
67 if (p[0].equals("usuario") && p.length == 3) {
68 docs = new ProxySeguridad(p[1], p[2], almacen);
69 System.out.println("Sesión de " + p[1] + " (" + p[2] + ")");
70 } else if (docs == null) {
71 System.out.println("Inicia sesión primero");
72 } else if (p[0].equals("leer") && p.length == 2) {
73 System.out.println(p[1] + ": " + docs.leer(p[1]));
74 } else if (p[0].equals("escribir") && p.length == 3) {
75 docs.escribir(p[1], p[2]);
76 System.out.println("Guardado: " + p[1]);
77 } else if (p[0].equals("borrar") && p.length == 2) {
78 docs.borrar(p[1]);
79 System.out.println("Borrado: " + p[1]);
80 } else {
81 System.out.println("Orden no válida: " + linea);
82 }
83 } catch (SecurityException e) {
84 denegados++;
85 System.out.println(e.getMessage());
86 } catch (IllegalArgumentException e) {
87 docs = null;
88 System.out.println(e.getMessage());
89 }
90 }
91 System.out.println("Intentos denegados: " + denegados + " · documentos: " + String.join(", ", almacen.nombres()));
92 }
93}El almacén no sabe nada de permisos: la seguridad está en un solo sitio y se aplica igual a todas las operaciones.
Mientras nadie tenga acceso directo al almacén, nadie se salta la comprobación: es la idea de la seguridad de métodos de Spring.
Test
Test: Proxy
0/5 respondidas · 0 aciertosElige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.
1.¿Qué tiene en común un proxy con el objeto real?
2.En un proxy virtual (carga perezosa), ¿cuándo se crea el objeto real?
3.¿Qué diferencia principal hay entre Proxy y Decorator?
4.¿Qué usa Hibernate para las relaciones perezosas?
5.¿Qué le falta a una caché que nunca se puede vaciar?