Tanquem la desfilada de patrons estructurals amb l'últim embolcall: un objecte que implementa la mateixa interfície que un altre i es fa passar per ell davant dels clients... no per traduir (Adapter) ni per afegir responsabilitats (Decorator), sinó per controlar l'accés a l'objecte real. De vegades el control és per economia (no carregar una foto de 3 MB fins que algú la miri), de vegades per seguretat (un operador no pot anul·lar comandes alienes a la seva zona), de vegades per distància (l'objecte real viu en un altre servidor), de vegades per memòria d'elefant (recordar respostes per no repetir viatges). A PideYa hi ha cua per a aquest patró: la carta triga a obrir-se perquè carrega totes les fotos, i el panell d'administració necessita permisos fins ja.
Contingut
- Intenció i estructura del patró
- Proxy virtual: les fotos dels plats
- Proxy de protecció: operadors i administradors
- Proxy de cache, de logging i remot
- Proxies dinàmics:
java.lang.reflect.Proxy - Proxy davant de Decorator
- Quan usar-lo i quan no
- Errors comuns, exercicis i conclusió
Intenció i estructura del patró
Intenció (GoF): proporcionar un substitut o representant d'un altre objecte per controlar-hi l'accés.
L'estructura és la d'un embolcall amb pedigrí: proxy i objecte real implementen la mateixa interfície (Subject), de manera que el client no distingeix —ni ha de poder distingir— a quin dels dos parla. El proxy desa una referència a l'objecte real (o la manera d'aconseguir-lo, que és la gràcia del proxy virtual) i decideix a cada crida si, quan i com delegar-la.
classDiagram
class ImatgePlat {
<<interface>>
+dimensions() Dimensions
+pintar(llenc: Llenc)
}
class ImatgePlatReal {
-pixels: byte[]
+dimensions() Dimensions
+pintar(llenc: Llenc)
}
class ProxyImatgePlat {
-url: String
-dimensionsConegudes: Dimensions
-real: ImatgePlatReal
+dimensions() Dimensions
+pintar(llenc: Llenc)
}
class AppClient
ImatgePlat <|.. ImatgePlatReal
ImatgePlat <|.. ProxyImatgePlat
ProxyImatgePlat o-- ImatgePlatReal : crea sota demanda
AppClient --> ImatgePlat : usa sense distingir
| Rol GoF | A PideYa |
|---|---|
| Subject (interfície comuna) | ImatgePlat |
| RealSubject (l'objecte de debò, car o sensible) | ImatgePlatReal |
| Proxy (el substitut que controla) | ProxyImatgePlat |
| Client | L'app que mostra la carta |
Sota aquesta única estructura viuen diversos patrons d'ús amb nom propi; la lliçó recorre els quatre que importen a la pràctica:
| Tipus de proxy | Què controla | A PideYa |
|---|---|---|
| Virtual | El quan: retarda crear/carregar l'objecte car fins al primer ús | Fotos de plats |
| De protecció | El qui: comprova permisos abans de delegar | Panell d'administració |
| De cache | El quantes vegades: recorda respostes d'operacions cares | Consultes al catàleg |
| Remot | L'on: representa localment un objecte d'un altre procés/màquina | Clients de serveis (menció) |
Proxy virtual: les fotos dels plats
El problema mesurat: la carta de La Bella Napoli té 60 plats amb foto (~3 MB cadascuna en origen). Carregar-les totes en obrir la carta són 180 MB i vuit segons d'espera... perquè l'usuari mitjà vegi una pantalla i mitja (unes 8 fotos). L'objecte real és car; l'ús, escàs: territori del proxy virtual.
public interface ImatgePlat {
Dimensions dimensions();
void pintar(Llenc llenc);
}
/** L'objecte car: descarrega i descodifica la foto EN CONSTRUIR-SE. */
public class ImatgePlatReal implements ImatgePlat {
private final byte[] pixels;
private final Dimensions dimensions;
public ImatgePlatReal(String url) {
this.pixels = Xarxa.descarregar(url); // 3 MB, centenars de ms
this.dimensions = Descodificador.mesurar(pixels);
}
@Override public Dimensions dimensions() { return dimensions; }
@Override public void pintar(Llenc llenc) { llenc.pintarPixels(pixels); }
}
/** El proxy: barat de crear; nomes materialitza el real quan de debo cal. */
public class ProxyImatgePlat implements ImatgePlat {
private final String url;
private final Dimensions dimensionsConegudes; // venen del cataleg: gratis!
private ImatgePlatReal real; // null fins al primer pintar()
public ProxyImatgePlat(String url, Dimensions dimensionsConegudes) {
this.url = url;
this.dimensionsConegudes = dimensionsConegudes;
}
/** Respon SENSE tocar l'objecte real: el layout de la carta no descarrega res. */
@Override
public Dimensions dimensions() { return dimensionsConegudes; }
/** Carrega mandrosa: el primer pintar paga la descarrega; els seguents, no. */
@Override
public void pintar(Llenc llenc) {
if (real == null) {
real = new ImatgePlatReal(url);
}
real.pintar(llenc);
}
}La carta ara construeix 60 proxies (60 objectes minúsculs, zero xarxa) i l'app maqueta amb dimensions() sense descarregar un byte; només quan un plat entra en pantalla, el seu pintar() materialitza la imatge real. Obertura instantània, i només es paga el que es mira. Dos matisos de qualitat:
- La jugada de
dimensions()és la meitat del valor: un bon proxy virtual respon el que pot sense molestar l'objecte real, i només delega l'inevitable. - La inicialització mandrosa aquí és el mateix problema que vam estudiar a fons a Singleton: en entorn multifil aquest
if (real == null)necessitaria la mateixa disciplina (sincronització o holder) que allà; no ho repetirem, però anota la connexió.
Proxy de protecció: operadors i administradors
Segon control: el qui. El panell intern de PideYa ofereix operacions de gestió de comandes, i no totes són per a tothom:
public interface GestioComandes {
Comanda consultar(NumeroComanda numero); // operador i administrador
void reemborsar(NumeroComanda numero); // nomes administrador
void anullar(NumeroComanda numero, String motiu); // nomes administrador
}En lloc de sembrar if (usuari.esAdmin()) pel servei real —barrejant seguretat amb negoci, i confiant que ningú no oblidi un if—, la comprovació es concentra en un proxy de protecció que embolcalla el servei real:
public class ProxyProteccioGestioComandes implements GestioComandes {
private final GestioComandes real; // el servei autentic
private final UsuariAutenticat usuari; // qui hi ha al teclat
public ProxyProteccioGestioComandes(GestioComandes real, UsuariAutenticat usuari) {
this.real = real;
this.usuari = usuari;
}
@Override
public Comanda consultar(NumeroComanda numero) {
return real.consultar(numero); // permes a tots els rols del panell
}
@Override
public void reemborsar(NumeroComanda numero) {
exigir(Rol.ADMINISTRADOR, "reemborsar " + numero);
real.reemborsar(numero);
}
@Override
public void anullar(NumeroComanda numero, String motiu) {
exigir(Rol.ADMINISTRADOR, "anullar " + numero);
real.anullar(numero, motiu);
}
private void exigir(Rol rol, String operacio) {
if (!usuari.te(rol)) {
RegistreEsdeveniments.INSTANCE.warn(usuari.getId() + " sense permis per a " + operacio);
throw new AccesDenegatException(operacio);
}
}
}L'arrel de composició lliura a cada sessió del panell el servei ja embolcallat segons el seu usuari; les pantalles programen contra GestioComandes sense saber si parlen amb el real o amb el guardià. El servei real queda net de seguretat (SRP), la política viu en un únic lloc auditable, i treure o endurir permisos no toca ni el servei ni les pantalles.
Proxy de cache, de logging i remot
Proxy de cache — controla el quantes vegades. La consulta de carta al servei de catàleg triga ~200 ms i la demanen milers de clients; la carta canvia unes poques vegades al dia:
public class ProxyCacheCataleg implements Cataleg {
private final Cataleg real;
private final Map<IdRestaurant, ComponentCarta> cache = new ConcurrentHashMap<>();
public ProxyCacheCataleg(Cataleg real) { this.real = real; }
@Override
public ComponentCarta cartaDe(IdRestaurant id) {
return cache.computeIfAbsent(id, real::cartaDe); // el viatge car, una vegada
}
/** El punt delicat de tota cache: invalidar quan el restaurant edita la carta. */
public void invalidar(IdRestaurant id) { cache.remove(id); }
}(Retorna l'arbre Composite de la carta, per cert: els patrons del mòdul ja viuen junts.) No confonguis aquest proxy amb la fàbrica de Flyweight: aquella compartia objectes immutables per estalviar memòria; aquest recorda resultats de crides cares per estalviar temps, i carrega amb el problema clàssic de la invalidació.
Proxy de logging: registra cada crida (qui, què, quant ha trigat) abans/després de delegar — mecànicament idèntic al NotificadorAmbLogging de la lliçó de Decorator, i aquest solapament no és casual: el disseccionem a la secció de cara a cara.
Proxy remot (només menció, amb desenvolupament al mòdul de sistemes distribuïts): l'objecte real viu en un altre procés o una altra màquina, i el proxy local ofereix la seva mateixa interfície ocultant xarxa, serialització i errors de transport. És el patró darrere d'RMI, dels stubs de gRPC i dels clients generats de les APIs REST: quan a PideYa el servei de repartiment se separi en el seu propi desplegament, el ServeiRepartiment que les altres peces continuïn veient serà un proxy remot.
Proxies dinàmics: java.lang.reflect.Proxy
Escriure a mà el proxy de logging per a GestioComandes, i per a Cataleg, i per a PassarellaPagament... és repetir la mateixa delegació amb un uniforme diferent. Java porta de sèrie la solució: generar el proxy en temps d'execució per a qualsevol interfície, amb la lògica transversal escrita una sola vegada en un InvocationHandler:
public class ManipuladorLogging implements InvocationHandler {
private final Object real;
public ManipuladorLogging(Object real) { this.real = real; }
@Override
public Object invoke(Object proxy, Method metode, Object[] args) throws Throwable {
long inici = System.nanoTime();
try {
Object resultat = metode.invoke(real, args); // delegar en l'objecte real
RegistreEsdeveniments.INSTANCE.info(metode.getName() + " OK en "
+ (System.nanoTime() - inici) / 1_000_000 + " ms");
return resultat;
} catch (InvocationTargetException e) {
RegistreEsdeveniments.INSTANCE.error(metode.getName() + " ha fallat: " + e.getCause());
throw e.getCause(); // re-llancar l'excepcio original
}
}
}// Un embolcall de logging per a QUALSEVOL interficie, sense escriure cap classe proxy:
GestioComandes ambLog = (GestioComandes) Proxy.newProxyInstance(
GestioComandes.class.getClassLoader(),
new Class<?>[] { GestioComandes.class },
new ManipuladorLogging(gestioReal));
Cataleg catalegAmbLog = (Cataleg) Proxy.newProxyInstance(
Cataleg.class.getClassLoader(),
new Class<?>[] { Cataleg.class },
new ManipuladorLogging(catalegReal));Proxy.newProxyInstance fabrica al vol una classe que implementa la interfície demanada i canalitza totes les crides cap a invoke(...). Es perd el tipatge fi dins del handler (tot és Method i Object[]) a canvi d'escriure el comportament transversal una vegada per a totes les interfícies.
Aquest mecanisme és la sala de màquines de mig ecosistema Java (menció per connectar amb el que ja uses o usaràs): Spring AOP embolcalla els teus beans en proxies dinàmics —així funcionen @Transactional, @Cacheable o la seguretat per anotacions: un proxy obre la transacció o comprova el rol abans de delegar en el teu mètode—; els repositoris de Spring Data que "no tenen implementació" són proxies dinàmics sobre la interfície que declares; Hibernate carrega les relacions mandroses lliurant proxies virtuals de les teves entitats; els mocks de Mockito són proxies que graven i verifiquen crides. Quan un framework sembli fer màgia al voltant dels teus mètodes, busca el proxy: gairebé sempre hi és.
Proxy davant de Decorator
La confusió clàssica del mòdul, perquè el diagrama és el mateix: implementar la interfície de l'embolcallat i delegar. Les diferències són en la intenció i en tres conseqüències pràctiques:
| Aspecte | Decorator | Proxy |
|---|---|---|
| Intenció | Afegir responsabilitats visibles a l'objecte | Controlar l'accés a l'objecte |
| Pot no delegar? | No: sempre crida l'embolcallat (afegeix al voltant) | Sí: pot negar (protecció), respondre pel seu compte (cache, dimensions()) o posposar (virtual) |
| Qui compon? | El client, apilant capes al seu gust (la gràcia és la combinatòria) | La infraestructura (arrel de composició, framework); el client ni sap que hi ha proxy |
| Qui coneix/crea l'embolcallat? | En rep un de ja creat, qualsevol de la interfície | Sovint sap crear o obtenir el seu objecte real (URL, lookup remot) |
| Cardinalitat típica | Moltes capes apilables en qualsevol ordre | Un proxy (o pocs i fixos) davant del real |
El cas frontera honest: un embolcall de logging. NotificadorAmbLogging era Decorator i ManipuladorLogging és Proxy? La mecànica és idèntica; la lectura correcta és per qui decideix i per a què: si és una capa opcional que el desenvolupador apila entre d'altres (responsabilitat afegida), parla de Decorator; si és un control que la infraestructura interposa d'ofici i el client ignora (accés vigilat/mesurat), parla de Proxy. A la frontera, el nom importa menys que entendre totes dues forces — i així ho tractarem a la comparativa final.
Quan usar-lo i quan no
Usa'l quan:
- Un objecte és car (memòria, xarxa, arrencada) i sovint no arriba a usar-se: proxy virtual (fotos, entitats mandroses, connexions).
- L'accés s'ha de vigilar o restringir segons qui crida: proxy de protecció (rols del panell).
- Operacions repetides i cares amb respostes estables: proxy de cache (catàleg), assumint el deure d'invalidar.
- L'objecte real és en una altra banda: proxy remot.
- Necessites comportament transversal a moltes interfícies (traces, mètriques, transaccions): proxies dinàmics, o el framework que ja els usa per tu.
No l'usis quan:
- No hi ha res per controlar: un proxy "per arquitectura" davant de cada servei és burocràcia — indirecció que tothom travessa i ningú no aprofita.
- El que vols és afegir comportament componible i visible: això és Decorator; o traduir una interfície aliena: Adapter; o simplificar moltes crides: Facade.
- La transparència seria una mentida perillosa: si el proxy remot pot trigar 30 segons o fallar per xarxa, fingir que és una crida local barata indueix a errors; de vegades és més sa que la interfície mostri l'asincronia o la fallada possible en el seu contracte.
Relació amb altres patrons (només menció): comparteix silueta amb Decorator i Adapter — l'acarament a quatre bandes dels embolcalls arriba a la pròxima lliçó; la càrrega mandrosa del proxy virtual reutilitza les tècniques d'inicialització de Singleton; les fàbriques del mòdul 2 són el lloc natural on decidir si el client rep l'objecte real o el seu proxy — que és exactament com ho fan els contenidors DI.
Errors Comuns i Consells
- Lògica de negoci al proxy. Si
ProxyProteccioGestioComandescomença a decidir quant reemborsar, ha envaït el negoci. El proxy decideix sobre l'accés (si/quan/qui); el què és sempre de l'objecte real. - Cache sense invalidació. El proxy de cache que mai no invalida serveix cartes d'ahir. Dissenya la invalidació (esdeveniment d'edició, TTL) al mateix commit que la cache, o tindràs el bug més difícil de reproduir del trimestre.
- Proxy virtual amb identitat traïdora.
equals/hashCode/toStringsobre el proxy sense materialitzar poden mentir o disparar la càrrega sense voler (untoStringen un log que descarrega 3 MB). Decideix què respon el proxy sense delegar i documenta-ho — el dolor clàssic amb les entitats mandroses de Hibernate. - Concurrència oblidada a la càrrega mandrosa. Dos fils, dues
ImatgePlatRealdescarregades. Ja coneixes l'arsenal del Singleton; usa'l a la mida del cas (importa carregar dues vegades? de vegades no). - Empassar-se la denegació. Un proxy de protecció que davant d'un accés denegat retorna
nullo no fa res converteix la seguretat en misteri ("premo reemborsar i no passa res"). Denegar és llançar i registrar, com hem fet, perquè la fallada sigui visible i auditable. - Consell: mantén el proxy fidel al contracte de la interfície: mateixes excepcions documentades, mateixa semàntica. El dia que el client necessita saber si hi ha proxy al davant, la transparència —l'actiu central del patró— s'ha perdut; si aquell dia arriba, replanteja el contracte en lloc d'acumular excepcions a la regla.
Exercicis
Exercici 1: proxy de només lectura per a repartidors
Els repartidors veuen les comandes que transporten a través de GestioComandes, però només han de poder consultar: qualsevol altra operació és un error de programació de l'app de repartidors que s'ha de detectar sorollosament. Escriu ProxyNomesConsulta i indica en què es diferencia la seva política de la del proxy de protecció per rols.
Exercici 2: llegir un proxy dinàmic
Amb el ManipuladorLogging de la lliçó, un company escriu:
GestioComandes g = (GestioComandes) Proxy.newProxyInstance(
GestioComandes.class.getClassLoader(),
new Class<?>[] { GestioComandes.class },
new ManipuladorLogging(new ProxyProteccioGestioComandes(gestioReal, usuari)));(a) Què es registra al log quan un operador sense rol d'administrador crida g.reemborsar(n): la denegació, l'intent, tots dos? (b) Què canviaria si s'invertís l'ordre dels embolcalls? (c) Quin ordre prefereixes per a una auditoria de seguretat?
Exercici 3: classificar controls
Per a cada necessitat, digues quin tipus de proxy aplica (o si el patró adequat és un altre): (1) que l'historial de posicions GPS d'un repartidor, enorme, només es porti de base de dades si el panell obre la seva pestanya; (2) traduir les crides de GestioComandes a l'SDK d'un partner logístic extern; (3) que les crides al servei de geocodificació (de pagament, per petició) no es repeteixin per a adreces ja resoltes; (4) que l'app de restaurant cridi el servei de comandes que ara viu en un altre desplegament.
Solucions
Solució 1:
public class ProxyNomesConsulta implements GestioComandes {
private final GestioComandes real;
public ProxyNomesConsulta(GestioComandes real) { this.real = real; }
@Override
public Comanda consultar(NumeroComanda numero) { return real.consultar(numero); }
@Override
public void reemborsar(NumeroComanda numero) {
throw new UnsupportedOperationException("L'app de repartidors es nomes de consulta");
}
@Override
public void anullar(NumeroComanda numero, String motiu) {
throw new UnsupportedOperationException("L'app de repartidors es nomes de consulta");
}
}Diferència de política: el proxy per rols decideix per usuari en temps d'execució (el mateix servei embolcallat serveix rols diferents; denegar és un esdeveniment de seguretat que es registra); aquest decideix per canal, incondicionalment (a aquella app mai no li correspon escriure; que ho intenti delata un bug del client, d'aquí UnsupportedOperationException en lloc d'AccesDenegatException). Mateix esquelet, contractes diferents.
Solució 2: (a) Tots dos com un sol esdeveniment: la crida entra pel proxy de logging (mesura), aquest delega en el de protecció, que denega llançant AccesDenegatException; l'excepció torna a travessar el logging, que registra "reemborsar ha fallat: AccesDenegatException". Es registra l'intent amb la seva denegació. (b) Invertit (protecció per fora), una crida denegada mai no arriba al logging: es rebutja abans, i el log només conté les crides permeses. (c) Per a auditoria de seguretat, l'ordre de l'enunciat (logging per fora): els intents denegats són precisament el que un auditor vol veure. Moralitat general: l'ordre dels embolcalls és semàntica, no estètica — ja ho vam veure amb els decoradors i torna a ser cert aquí.
Solució 3: (1) Proxy virtual: objecte car (historial enorme) materialitzat només al primer accés. (2) Cap proxy: és Adapter — cal traduir entre dues interfícies diferents, no controlar l'accés darrere de la mateixa interfície. (3) Proxy de cache, amb invalidació tranquil·la (les adreces resoltes no caduquen a efectes pràctics): estalvi directe a la factura. (4) Proxy remot: mateixa interfície ServeiComandes, transport amagat — amb l'honestedat de contracte que hem discutit (temps i fallades de xarxa existeixen).
Conclusió
Proxy completa els embolcalls amb la intenció de control: mateixa interfície que l'objecte real i poder de decisió sobre cada crida — posposar-la (virtual), negar-la (protecció), respondre-la de memòria (cache), creuar la xarxa per tu (remot) o mesurar-la (logging). A PideYa van quedar el ProxyImatgePlat que va fer instantània la carta, el ProxyProteccioGestioComandes que va concentrar els permisos del panell i el ProxyCacheCataleg sobre l'arbre de la carta; i amb java.lang.reflect.Proxy has vist la versió industrial del patró, la que sosté Spring, Hibernate i Mockito. També va quedar traçada la frontera fina amb Decorator: afegir davant de controlar, client que apila davant d'infraestructura que interposa.
I amb aquest ja són set: adaptar, pontejar, compondre en arbre, decorar, simplificar, compartir i controlar. Set respostes estructurals que ara demanen el mateix que van demanar els creacionals al final del seu mòdul: posar-les cara a cara, aprendre a triar entre les que s'assemblen —aquells quatre embolcalls gairebé bessons—, veure com es combinen i repassar el mapa complet d'on va quedar cadascuna a PideYa. Ens veiem a la Comparativa i Elecció de Patrons Estructurals.
Curs de Patrons de Disseny de Programari
Mòdul 1: Introducció als Patrons de Disseny
- Què són els Patrons de Disseny?
- Història i Origen dels Patrons de Disseny
- Principis de Disseny: SOLID i Altres Fonaments
- UML Essencial per Entendre Patrons
- Classificació dels Patrons de Disseny
- Avantatges i Desavantatges d'Usar Patrons de Disseny
Mòdul 2: Patrons Creacionals
- Introducció als Patrons Creacionals
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa i Elecció de Patrons Creacionals
Mòdul 3: Patrons Estructurals
- Introducció als Patrons Estructurals
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa i Elecció de Patrons Estructurals
Mòdul 4: Patrons de Comportament
- Introducció als Patrons de Comportament
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa i Elecció de Patrons de Comportament
Mòdul 5: Aplicació de Patrons de Disseny
- Com Seleccionar el Patró Adequat
- Exemples Pràctics d'Ús de Patrons
- Patrons de Disseny en Projectes Reals
- Refactorització Usant Patrons de Disseny
- Antipatrons: Quan els Patrons es Tornen un Problema
Mòdul 6: Patrons de Disseny Avançats
- Patrons de Disseny en Arquitectures Modernes
- Patrons de Disseny en Microserveis
- Patrons de Disseny en Sistemes Distribuïts
- Patrons de Concurrència
- Patrons de Disseny en Desenvolupament Àgil
