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

  1. Intenció i estructura del patró
  2. Proxy virtual: les fotos dels plats
  3. Proxy de protecció: operadors i administradors
  4. Proxy de cache, de logging i remot
  5. Proxies dinàmics: java.lang.reflect.Proxy
  6. Proxy davant de Decorator
  7. Quan usar-lo i quan no
  8. 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 ProxyProteccioGestioComandes començ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/toString sobre el proxy sense materialitzar poden mentir o disparar la càrrega sense voler (un toString en 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 ImatgePlatReal descarregades. 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 null o 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

Mòdul 2: Patrons Creacionals

Mòdul 3: Patrons Estructurals

Mòdul 4: Patrons de Comportament

Mòdul 5: Aplicació de Patrons de Disseny

Mòdul 6: Patrons de Disseny Avançats

Mòdul 7: Recursos Addicionals i Conclusió

© Copyright 2026. Tots els drets reservats