Cerramos el desfile de patrones estructurales con el último envoltorio: un objeto que implementa la misma interfaz que otro y se hace pasar por él ante los clientes... no para traducir (Adapter) ni para añadir responsabilidades (Decorator), sino para controlar el acceso al objeto real. A veces el control es por economía (no cargar una foto de 3 MB hasta que alguien la mire), a veces por seguridad (un operador no puede anular pedidos ajenos a su zona), a veces por distancia (el objeto real vive en otro servidor), a veces por memoria de elefante (recordar respuestas para no repetir viajes). En PideYa hay cola para este patrón: la carta tarda en abrir porque carga todas las fotos, y el panel de administración necesita permisos finos ya.
Contenido
- Intención y estructura del patrón
- Proxy virtual: las fotos de los platos
- Proxy de protección: operadores y administradores
- Proxy de caché, de logging y remoto
- Proxies dinámicos:
java.lang.reflect.Proxy - Proxy frente a Decorator
- Cuándo usarlo y cuándo no
- Errores comunes, ejercicios y conclusión
Intención y estructura del patrón
Intención (GoF): proporcionar un sustituto o representante de otro objeto para controlar el acceso a él.
La estructura es la de un envoltorio con pedigrí: proxy y objeto real implementan la misma interfaz (Subject), de modo que el cliente no distingue —ni debe poder distinguir— a cuál de los dos habla. El proxy guarda una referencia al objeto real (o la forma de conseguirlo, que es la gracia del proxy virtual) y decide en cada llamada si, cuándo y cómo delegarla.
classDiagram
class ImagenPlato {
<<interface>>
+dimensiones() Dimensiones
+pintar(lienzo: Lienzo)
}
class ImagenPlatoReal {
-pixeles: byte[]
+dimensiones() Dimensiones
+pintar(lienzo: Lienzo)
}
class ProxyImagenPlato {
-url: String
-dimensionesConocidas: Dimensiones
-real: ImagenPlatoReal
+dimensiones() Dimensiones
+pintar(lienzo: Lienzo)
}
class AppCliente
ImagenPlato <|.. ImagenPlatoReal
ImagenPlato <|.. ProxyImagenPlato
ProxyImagenPlato o-- ImagenPlatoReal : crea bajo demanda
AppCliente --> ImagenPlato : usa sin distinguir
| Rol GoF | En PideYa |
|---|---|
| Subject (interfaz común) | ImagenPlato |
| RealSubject (el objeto de verdad, caro o sensible) | ImagenPlatoReal |
| Proxy (el sustituto que controla) | ProxyImagenPlato |
| Client | La app que muestra la carta |
Bajo esta única estructura viven varios patrones de uso con nombre propio; la lección recorre los cuatro que importan en la práctica:
| Tipo de proxy | Qué controla | En PideYa |
|---|---|---|
| Virtual | El cuándo: retrasa crear/cargar el objeto caro hasta el primer uso | Fotos de platos |
| De protección | El quién: comprueba permisos antes de delegar | Panel de administración |
| De caché | El cuántas veces: recuerda respuestas de operaciones caras | Consultas al catálogo |
| Remoto | El dónde: representa localmente un objeto de otro proceso/máquina | Clientes de servicios (mención) |
Proxy virtual: las fotos de los platos
El problema medido: la carta de La Bella Napoli tiene 60 platos con foto (~3 MB cada una en origen). Cargarlas todas al abrir la carta son 180 MB y ocho segundos de espera... para que el usuario medio vea una pantalla y media (unas 8 fotos). El objeto real es caro; el uso, escaso: territorio del proxy virtual.
public interface ImagenPlato {
Dimensiones dimensiones();
void pintar(Lienzo lienzo);
}
/** El objeto caro: descarga y decodifica la foto AL CONSTRUIRSE. */
public class ImagenPlatoReal implements ImagenPlato {
private final byte[] pixeles;
private final Dimensiones dimensiones;
public ImagenPlatoReal(String url) {
this.pixeles = Red.descargar(url); // 3 MB, cientos de ms
this.dimensiones = Decodificador.medir(pixeles);
}
@Override public Dimensiones dimensiones() { return dimensiones; }
@Override public void pintar(Lienzo lienzo) { lienzo.pintarPixeles(pixeles); }
}
/** El proxy: barato de crear; solo materializa el real cuando de verdad hace falta. */
public class ProxyImagenPlato implements ImagenPlato {
private final String url;
private final Dimensiones dimensionesConocidas; // vienen del catálogo: ¡gratis!
private ImagenPlatoReal real; // null hasta el primer pintar()
public ProxyImagenPlato(String url, Dimensiones dimensionesConocidas) {
this.url = url;
this.dimensionesConocidas = dimensionesConocidas;
}
/** Responde SIN tocar el objeto real: el layout de la carta no descarga nada. */
@Override
public Dimensiones dimensiones() { return dimensionesConocidas; }
/** Carga perezosa: el primer pintar paga la descarga; los siguientes, no. */
@Override
public void pintar(Lienzo lienzo) {
if (real == null) {
real = new ImagenPlatoReal(url);
}
real.pintar(lienzo);
}
}La carta ahora construye 60 proxies (60 objetos minúsculos, cero red) y la app maqueta con dimensiones() sin descargar un byte; solo cuando un plato entra en pantalla, su pintar() materializa la imagen real. Apertura instantánea, y solo se paga lo que se mira. Dos matices de calidad:
- La jugada de
dimensiones()es la mitad del valor: un buen proxy virtual responde lo que puede sin molestar al objeto real, y solo delega lo inevitable. - La inicialización perezosa aquí es el mismo problema que estudiamos a fondo en Singleton: en entorno multihilo este
if (real == null)necesitaría la misma disciplina (sincronización o holder) que allí; no lo repetiremos, pero anota la conexión.
Proxy de protección: operadores y administradores
Segundo control: el quién. El panel interno de PideYa ofrece operaciones de gestión de pedidos, y no todas son para todos:
public interface GestionPedidos {
Pedido consultar(NumeroPedido numero); // operador y administrador
void reembolsar(NumeroPedido numero); // solo administrador
void anular(NumeroPedido numero, String motivo); // solo administrador
}En lugar de sembrar if (usuario.esAdmin()) por el servicio real —mezclando seguridad con negocio, y confiando en que nadie olvide un if—, la comprobación se concentra en un proxy de protección que envuelve al servicio real:
public class ProxyProteccionGestionPedidos implements GestionPedidos {
private final GestionPedidos real; // el servicio auténtico
private final UsuarioAutenticado usuario; // quién está al teclado
public ProxyProteccionGestionPedidos(GestionPedidos real, UsuarioAutenticado usuario) {
this.real = real;
this.usuario = usuario;
}
@Override
public Pedido consultar(NumeroPedido numero) {
return real.consultar(numero); // permitido a todos los roles del panel
}
@Override
public void reembolsar(NumeroPedido numero) {
exigir(Rol.ADMINISTRADOR, "reembolsar " + numero);
real.reembolsar(numero);
}
@Override
public void anular(NumeroPedido numero, String motivo) {
exigir(Rol.ADMINISTRADOR, "anular " + numero);
real.anular(numero, motivo);
}
private void exigir(Rol rol, String operacion) {
if (!usuario.tiene(rol)) {
RegistroEventos.INSTANCE.warn(usuario.getId() + " sin permiso para " + operacion);
throw new AccesoDenegadoException(operacion);
}
}
}La raíz de composición entrega a cada sesión del panel el servicio ya envuelto según su usuario; las pantallas programan contra GestionPedidos sin saber si hablan con el real o con el guardián. El servicio real queda limpio de seguridad (SRP), la política vive en un único sitio auditable, y quitar o endurecer permisos no toca ni al servicio ni a las pantallas.
Proxy de caché, de logging y remoto
Proxy de caché — controla el cuántas veces. La consulta de carta al servicio de catálogo tarda ~200 ms y la piden miles de clientes; la carta cambia unas pocas veces al día:
public class ProxyCacheCatalogo implements Catalogo {
private final Catalogo real;
private final Map<IdRestaurante, ComponenteCarta> cache = new ConcurrentHashMap<>();
public ProxyCacheCatalogo(Catalogo real) { this.real = real; }
@Override
public ComponenteCarta cartaDe(IdRestaurante id) {
return cache.computeIfAbsent(id, real::cartaDe); // el viaje caro, una vez
}
/** El punto delicado de todo caché: invalidar cuando el restaurante edita su carta. */
public void invalidar(IdRestaurante id) { cache.remove(id); }
}(Devuelve el árbol Composite de la carta, por cierto: los patrones del módulo ya viven juntos.) No confundas este proxy con la fábrica de Flyweight: aquella compartía objetos inmutables para ahorrar memoria; este recuerda resultados de llamadas caras para ahorrar tiempo, y carga con el problema clásico de la invalidación.
Proxy de logging: registra cada llamada (quién, qué, cuánto tardó) antes/después de delegar — mecánicamente idéntico al NotificadorConLogging de la lección de Decorator, y ese solapamiento no es casual: lo diseccionamos en la sección de frente a frente.
Proxy remoto (solo mención, con desarrollo en el módulo de sistemas distribuidos): el objeto real vive en otro proceso u otra máquina, y el proxy local ofrece su misma interfaz ocultando red, serialización y errores de transporte. Es el patrón detrás de RMI, de los stubs de gRPC y de los clientes generados de las APIs REST: cuando en PideYa el servicio de reparto se separe en su propio despliegue, el ServicioReparto que las demás piezas sigan viendo será un proxy remoto.
Proxies dinámicos: java.lang.reflect.Proxy
Escribir a mano el proxy de logging para GestionPedidos, y para Catalogo, y para PasarelaPago... es repetir la misma delegación con distinto uniforme. Java trae de serie la solución: generar el proxy en tiempo de ejecución para cualquier interfaz, con la lógica transversal escrita una sola vez en un InvocationHandler:
public class ManejadorLogging implements InvocationHandler {
private final Object real;
public ManejadorLogging(Object real) { this.real = real; }
@Override
public Object invoke(Object proxy, Method metodo, Object[] args) throws Throwable {
long inicio = System.nanoTime();
try {
Object resultado = metodo.invoke(real, args); // delegar en el objeto real
RegistroEventos.INSTANCE.info(metodo.getName() + " OK en "
+ (System.nanoTime() - inicio) / 1_000_000 + " ms");
return resultado;
} catch (InvocationTargetException e) {
RegistroEventos.INSTANCE.error(metodo.getName() + " falló: " + e.getCause());
throw e.getCause(); // re-lanzar la excepción original
}
}
}// Un envoltorio de logging para CUALQUIER interfaz, sin escribir ninguna clase proxy:
GestionPedidos conLog = (GestionPedidos) Proxy.newProxyInstance(
GestionPedidos.class.getClassLoader(),
new Class<?>[] { GestionPedidos.class },
new ManejadorLogging(gestionReal));
Catalogo catalogoConLog = (Catalogo) Proxy.newProxyInstance(
Catalogo.class.getClassLoader(),
new Class<?>[] { Catalogo.class },
new ManejadorLogging(catalogoReal));Proxy.newProxyInstance fabrica al vuelo una clase que implementa la interfaz pedida y canaliza todas las llamadas hacia invoke(...). Se pierde el tipado fino dentro del handler (todo es Method y Object[]) a cambio de escribir el comportamiento transversal una vez para todas las interfaces.
Este mecanismo es la sala de máquinas de medio ecosistema Java (mención para conectar con lo que ya usas o usarás): Spring AOP envuelve tus beans en proxies dinámicos —así funcionan @Transactional, @Cacheable o la seguridad por anotaciones: un proxy abre la transacción o comprueba el rol antes de delegar en tu método—; los repositorios de Spring Data que "no tienen implementación" son proxies dinámicos sobre la interfaz que declaras; Hibernate carga las relaciones perezosas entregando proxies virtuales de tus entidades; los mocks de Mockito son proxies que graban y verifican llamadas. Cuando un framework parezca hacer magia alrededor de tus métodos, busca el proxy: casi siempre está.
Proxy frente a Decorator
La confusión clásica del módulo, porque el diagrama es el mismo: implementar la interfaz del envuelto y delegar. Las diferencias están en la intención y en tres consecuencias prácticas:
| Aspecto | Decorator | Proxy |
|---|---|---|
| Intención | Añadir responsabilidades visibles al objeto | Controlar el acceso al objeto |
| ¿Puede no delegar? | No: siempre llama al envuelto (añade alrededor) | Sí: puede negar (protección), responder por su cuenta (caché, dimensiones()) o posponer (virtual) |
| ¿Quién compone? | El cliente, apilando capas a su gusto (la gracia es la combinatoria) | La infraestructura (raíz de composición, framework); el cliente ni sabe que hay proxy |
| ¿Quién conoce/crea al envuelto? | Recibe uno ya creado, cualquiera de la interfaz | A menudo sabe crear u obtener a su objeto real (URL, lookup remoto) |
| Cardinalidad típica | Muchas capas apilables en cualquier orden | Un proxy (o pocos y fijos) delante del real |
El caso frontera honesto: un envoltorio de logging. ¿NotificadorConLogging era Decorator y ManejadorLogging es Proxy? La mecánica es idéntica; la lectura correcta es por quién decide y para qué: si es una capa opcional que el desarrollador apila entre otras (responsabilidad añadida), habla de Decorator; si es un control que la infraestructura interpone de oficio y el cliente ignora (acceso vigilado/medido), habla de Proxy. En la frontera, el nombre importa menos que entender ambas fuerzas — y así lo trataremos en la comparativa final.
Cuándo usarlo y cuándo no
Úsalo cuando:
- Un objeto es caro (memoria, red, arranque) y a menudo no llega a usarse: proxy virtual (fotos, entidades perezosas, conexiones).
- El acceso debe vigilarse o restringirse según quién llama: proxy de protección (roles del panel).
- Operaciones repetidas y caras con respuestas estables: proxy de caché (catálogo), asumiendo el deber de invalidar.
- El objeto real está en otra parte: proxy remoto.
- Necesitas comportamiento transversal a muchas interfaces (trazas, métricas, transacciones): proxies dinámicos, o el framework que ya los usa por ti.
No lo uses cuando:
- No hay nada que controlar: un proxy "por arquitectura" delante de cada servicio es burocracia — indirección que todo el mundo atraviesa y nadie aprovecha.
- Lo que quieres es añadir comportamiento componible y visible: eso es Decorator; o traducir una interfaz ajena: Adapter; o simplificar muchas llamadas: Facade.
- La transparencia sería una mentira peligrosa: si el proxy remoto puede tardar 30 segundos o fallar por red, fingir que es una llamada local barata induce a errores; a veces es más sano que la interfaz muestre la asincronía o el fallo posible en su contrato.
Relación con otros patrones (solo mención): comparte silueta con Decorator y Adapter — el careo a cuatro bandas de los envoltorios llega en la próxima lección; la carga perezosa del proxy virtual reutiliza las técnicas de inicialización de Singleton; las fábricas del módulo 2 son el lugar natural donde decidir si el cliente recibe el objeto real o su proxy — que es exactamente como lo hacen los contenedores DI.
Errores Comunes y Consejos
- Lógica de negocio en el proxy. Si
ProxyProteccionGestionPedidosempieza a decidir cuánto reembolsar, ha invadido el negocio. El proxy decide sobre el acceso (si/cuándo/quién); el qué es siempre del objeto real. - Caché sin invalidación. El proxy de caché que nunca invalida sirve cartas de ayer. Diseña la invalidación (evento de edición, TTL) en el mismo commit que la caché, o tendrás el bug más difícil de reproducir del trimestre.
- Proxy virtual con identidad traicionera.
equals/hashCode/toStringsobre el proxy sin materializar pueden mentir o disparar la carga sin querer (untoStringen un log que descarga 3 MB). Decide qué responde el proxy sin delegar y documéntalo — el dolor clásico con las entidades perezosas de Hibernate. - Concurrencia olvidada en la carga perezosa. Dos hilos, dos
ImagenPlatoRealdescargados. Ya conoces el arsenal del Singleton; úsalo a la medida del caso (¿importa cargar dos veces? a veces no). - Tragarse la denegación. Un proxy de protección que ante un acceso denegado devuelve
nullo no hace nada convierte la seguridad en misterio ("le doy a reembolsar y no pasa nada"). Denegar es lanzar y registrar, como hicimos, para que el fallo sea visible y auditable. - Consejo: mantén el proxy fiel al contrato de la interfaz: mismas excepciones documentadas, misma semántica. El día que el cliente necesita saber si hay proxy delante, la transparencia —el activo central del patrón— se ha perdido; si ese día llega, replantea el contrato en lugar de acumular excepciones a la regla.
Ejercicios
Ejercicio 1: proxy de solo lectura para repartidores
Los repartidores ven los pedidos que transportan a través de GestionPedidos, pero solo deben poder consultar: cualquier otra operación es un error de programación de la app de repartidores que debe detectarse ruidosamente. Escribe ProxySoloConsulta e indica en qué se diferencia su política de la del proxy de protección por roles.
Ejercicio 2: leer un proxy dinámico
Con el ManejadorLogging de la lección, un compañero escribe:
GestionPedidos g = (GestionPedidos) Proxy.newProxyInstance(
GestionPedidos.class.getClassLoader(),
new Class<?>[] { GestionPedidos.class },
new ManejadorLogging(new ProxyProteccionGestionPedidos(gestionReal, usuario)));(a) ¿Qué se registra en el log cuando un operador sin rol de administrador llama a g.reembolsar(n): la denegación, el intento, ambos? (b) ¿Qué cambiaría si se invirtiera el orden de los envoltorios? (c) ¿Qué orden prefieres para una auditoría de seguridad?
Ejercicio 3: clasificar controles
Para cada necesidad, di qué tipo de proxy aplica (o si el patrón adecuado es otro): (1) que el historial de posiciones GPS de un repartidor, enorme, solo se traiga de base de datos si el panel abre su pestaña; (2) traducir las llamadas de GestionPedidos al SDK de un partner logístico externo; (3) que las llamadas al servicio de geocodificación (de pago, por petición) no se repitan para direcciones ya resueltas; (4) que la app de restaurante llame al servicio de comandas que ahora vive en otro despliegue.
Soluciones
Solución 1:
public class ProxySoloConsulta implements GestionPedidos {
private final GestionPedidos real;
public ProxySoloConsulta(GestionPedidos real) { this.real = real; }
@Override
public Pedido consultar(NumeroPedido numero) { return real.consultar(numero); }
@Override
public void reembolsar(NumeroPedido numero) {
throw new UnsupportedOperationException("La app de repartidores es de solo consulta");
}
@Override
public void anular(NumeroPedido numero, String motivo) {
throw new UnsupportedOperationException("La app de repartidores es de solo consulta");
}
}Diferencia de política: el proxy por roles decide por usuario en tiempo de ejecución (el mismo servicio envuelto sirve a roles distintos; denegar es un evento de seguridad que se registra); este decide por canal, incondicionalmente (a esa app jamás le corresponde escribir; que lo intente delata un bug del cliente, de ahí UnsupportedOperationException en vez de AccesoDenegadoException). Mismo esqueleto, contratos distintos.
Solución 2: (a) Ambos como un solo evento: la llamada entra por el proxy de logging (mide), este delega en el de protección, que deniega lanzando AccesoDenegadoException; la excepción vuelve a atravesar el logging, que registra "reembolsar falló: AccesoDenegadoException". Se loguea el intento con su denegación. (b) Invertido (protección por fuera), una llamada denegada nunca llega al logging: se rechaza antes, y el log solo contiene las llamadas permitidas. (c) Para auditoría de seguridad, el orden del enunciado (logging por fuera): los intentos denegados son precisamente lo que un auditor quiere ver. Moraleja general: el orden de los envoltorios es semántica, no estética — ya lo vimos con los decoradores y vuelve a ser cierto aquí.
Solución 3: (1) Proxy virtual: objeto caro (historial enorme) materializado solo al primer acceso. (2) Ningún proxy: es Adapter — hay que traducir entre dos interfaces distintas, no controlar el acceso tras la misma interfaz. (3) Proxy de caché, con invalidación tranquila (las direcciones resueltas no caducan a efectos prácticos): ahorro directo en la factura. (4) Proxy remoto: misma interfaz ServicioComandas, transporte escondido — con la honestidad de contrato que discutimos (tiempos y fallos de red existen).
Conclusión
Proxy completa los envoltorios con la intención de control: misma interfaz que el objeto real y poder de decisión sobre cada llamada — posponerla (virtual), negarla (protección), responderla de memoria (caché), cruzar la red por ti (remoto) o medirla (logging). En PideYa quedaron el ProxyImagenPlato que hizo instantánea la carta, el ProxyProteccionGestionPedidos que concentró los permisos del panel y el ProxyCacheCatalogo sobre el árbol de la carta; y con java.lang.reflect.Proxy viste la versión industrial del patrón, la que sostiene a Spring, Hibernate y Mockito. También quedó trazada la frontera fina con Decorator: añadir frente a controlar, cliente que apila frente a infraestructura que interpone.
Y con este son siete: adaptar, puentear, componer en árbol, decorar, simplificar, compartir y controlar. Siete respuestas estructurales que ahora piden lo mismo que pidieron los creacionales al final de su módulo: ponerlas frente a frente, aprender a elegir entre las que se parecen —esos cuatro envoltorios casi gemelos—, ver cómo se combinan y repasar el mapa completo de dónde quedó cada una en PideYa. Nos vemos en la Comparativa y Elección de Patrones Estructurales.
Curso de Patrones de Diseño de Software
Módulo 1: Introducción a los Patrones de Diseño
- ¿Qué son los Patrones de Diseño?
- Historia y Origen de los Patrones de Diseño
- Principios de Diseño: SOLID y Otros Fundamentos
- UML Esencial para Entender Patrones
- Clasificación de los Patrones de Diseño
- Ventajas y Desventajas de Usar Patrones de Diseño
Módulo 2: Patrones Creacionales
- Introducción a los Patrones Creacionales
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa y Elección de Patrones Creacionales
Módulo 3: Patrones Estructurales
- Introducción a los Patrones Estructurales
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa y Elección de Patrones Estructurales
Módulo 4: Patrones de Comportamiento
- Introducción a los Patrones de Comportamiento
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa y Elección de Patrones de Comportamiento
Módulo 5: Aplicación de Patrones de Diseño
- Cómo Seleccionar el Patrón Adecuado
- Ejemplos Prácticos de Uso de Patrones
- Patrones de Diseño en Proyectos Reales
- Refactorización Usando Patrones de Diseño
- Antipatrones: Cuándo los Patrones se Vuelven un Problema
Módulo 6: Patrones de Diseño Avanzados
- Patrones de Diseño en Arquitecturas Modernas
- Patrones de Diseño en Microservicios
- Patrones de Diseño en Sistemas Distribuidos
- Patrones de Concurrencia
- Patrones de Diseño en Desarrollo Ágil
