Apuntes DAM

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

  1. Sujeto. La interfaz común (Imagen) que usa el cliente.
  2. Sujeto real. La clase que hace el trabajo (ImagenReal), cara de crear o de llamar.
  3. Proxy. Implementa la misma interfaz, guarda una referencia al sujeto real (que puede ser null hasta que haga falta) y controla cada llamada antes de delegar.
  4. Cliente. Usa la interfaz y no sabe si habla con el proxy o con el objeto real.

Diagrama de clases

0..1real0..*fotos«interface»Imagen+nombre() : String+mostrar() : voidImagenReal-nombre : String+ImagenReal(nombre : String)ImagenProxy-nombre : String-real : ImagenRealGaleria
Arrastra las clases para colocarlas a tu gusto.
Ver el diagrama en PlantUML
text
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@enduml

Puedes 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.

Java
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}

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.

Java
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.

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, @Async y 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 ciudad y olvidar 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.
JavaCaché de un servicio del tiempoFácil

Ejemplo

Entrada (lo que se escribe por teclado)
prevision Madrid
prevision Bilbao
prevision Madrid
prevision Madrid
prevision Bilbao
Salida esperada
(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
Test oculto #3
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
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 texto y borrar doc. Al final, Intentos denegados: n · documentos: lista (el main cuenta las excepciones).
  • Roles: lector puede leer; editor, leer y escribir; admin, todo. Un rol que no existe: IllegalArgumentException con Rol desconocido: rol (al crear el proxy).
  • Mensaje: Acceso denegado: nombre (rol) no puede acción doc.
JavaDocumentos con permisosMedio

Ejemplo

Entrada (lo que se escribe por teclado)
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
Salida esperada
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
Test oculto #3
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
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 aciertos

Elige 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. 1.¿Qué tiene en común un proxy con el objeto real?

  2. 2.En un proxy virtual (carga perezosa), ¿cuándo se crea el objeto real?

  3. 3.¿Qué diferencia principal hay entre Proxy y Decorator?

  4. 4.¿Qué usa Hibernate para las relaciones perezosas?

  5. 5.¿Qué le falta a una caché que nunca se puede vaciar?

Relacionado