Fins aquí, els estructurals han organitzat peces comptades: un adaptador, dues jerarquies, un arbre de carta, unes capes d'extres, una façana. Flyweight entra en escena quan les peces es compten per desenes de milers i la memòria comença a protestar. És el patró més especialitzat del mòdul —el que menys vegades aplicaràs— i alhora un que uses cada dia sense saber-ho, perquè el mateix Java el porta de sèrie. La seva idea cap en una frase: si deu mil objectes repeteixen les mateixes dades, que els deu mil comparteixin una sola còpia d'allò repetit i carregui cadascun només amb allò que de debò li és propi.

Contingut

  1. El problema a PideYa: el mapa en temps real
  2. Estat intrínsec i estat extrínsec
  3. Intenció i estructura del patró
  4. Implementació Java completa
  5. Flyweight al JDK: Integer.valueOf i l'string pool
  6. Quan NO val la pena
  7. Errors comuns, exercicis i conclusió

El problema a PideYa: el mapa en temps real

El centre d'operacions de PideYa mostra un mapa de la ciutat amb tots els repartidors i restaurants en viu: a Madrid en hora punta, uns 4.000 repartidors (en bici, en moto, en cotxe, cada tipus amb la seva icona i el seu color segons estat: disponible, repartint, en pausa) i 9.000 restaurants (icona per categoria: pizzeria, sushi, hamburgueses...). Cada marcador es refresca diverses vegades per segon. El primer model, un a un:

// Primer intent: cada marcador carrega amb TOT. NO imitar.
public class MarcadorMapa {
    // El que canvia per marcador:
    private String id;                 // "repartidor-8841"
    private double latitud;
    private double longitud;

    // El que es repeteix en milers de marcadors identics:
    private byte[] icona;              // PNG de la icona... 32 KB!
    private String etiquetaTipus;      // "Repartidor en moto"
    private Color color;               // segons estat
    private int amplePx, altPx;        // mida de renderitzacio
}

El compte és demolidor: 13.000 marcadors × ~32 KB d'icona ≈ 400 MB només en icones... quan en realitat només existeixen unes desenes de combinacions diferents de (icona, etiqueta, color, mida): moto-disponible, moto-repartint, bici-disponible, pizzeria, sushi... Tota la resta és la mateixa informació duplicada milers de vegades. A l'app del client (mòbils modestos) el mapa directament no hi cap; al panell d'operacions, el recol·lector d'escombraries no aixeca el cap.

El malbaratament té una estructura precisa: cada marcador barreja dues classes d'estat amb vides completament diferents. Separar-les és tot el patró.

Estat intrínsec i estat extrínsec

La distinció central de Flyweight, que convé dominar abans de veure una línia de codi:

Estat intrínsec Estat extrínsec
Què és El que l'objecte és pel seu tipus: igual en tots els exemplars d'aquella classe de marcador El que l'objecte és pel seu context: diferent a cada aparició
Al mapa Icona, etiqueta de tipus, color d'estat, mida Id del repartidor/restaurant, latitud, longitud
Depèn del context? No: "moto-disponible" és igual aquí i a Vallecas Sí: cada marcador té la seva
Mutable? Mai: ha de ser immutable per compartir-se Lliurement: canvia amb cada refresc de GPS
On viu Dins del flyweight, una sola còpia compartida Fora: l'aporta el client a cada crida
Quants n'hi ha Desenes (combinacions de tipus × estat) Milers (un per entitat al mapa)

La recepta del patró: extreure l'estat intrínsec a objectes compartits i immutables (els flyweights), treure l'extrínsec fora (el client el passa com a paràmetres a cada operació), i posar una fàbrica que garanteixi que cada combinació intrínseca es crea una sola vegada.

Intenció i estructura del patró

Intenció (GoF): usar compartició per donar suport eficient a grans quantitats d'objectes de gra fi.

classDiagram
    class IconaMarcador {
        <<flyweight>>
        -icona: byte[]
        -etiquetaTipus: String
        -color: Color
        -amplePx: int
        -altPx: int
        +dibuixar(llenc: Llenc, latitud: double, longitud: double)
    }
    class FabricaIcones {
        -cache: Map~ClauIcona, IconaMarcador~
        +obtenir(tipus: TipusEntitat, estat: Estat) IconaMarcador
        +midaCache() int
    }
    class MarcadorMapa {
        -id: String
        -latitud: double
        -longitud: double
        -icona: IconaMarcador
        +dibuixar(llenc: Llenc)
    }
    class PanellOperacions

    FabricaIcones o-- IconaMarcador : fa cache i comparteix
    MarcadorMapa --> IconaMarcador : referencia compartida
    PanellOperacions --> FabricaIcones : obte flyweights
    PanellOperacions --> MarcadorMapa : en mante milers
Rol GoF A PideYa
Flyweight (estat intrínsec compartit, immutable) IconaMarcador
FlyweightFactory (crea o reutilitza, fa cache) FabricaIcones
Client / context (desa l'estat extrínsec) MarcadorMapa (id, posició) i el panell

Dues observacions sobre el diagrama:

  • L'operació del flyweight (dibuixar) rep l'estat extrínsec per paràmetres: el flyweight no sap on és, l'hi diuen a cada crida. Aquesta firma és el segell del patró.
  • L'objecte de context (MarcadorMapa) queda reduït al mínim: id, dos doubles i una referència al flyweight compartit. De 32 KB a unes desenes de bytes per marcador.

Implementació Java completa

El flyweight — immutable a consciència, condició innegociable per compartir-lo (fins i tot entre fils, gratis):

public final class IconaMarcador {

    private final byte[] icona;          // els 32 KB, UNA vegada per combinacio
    private final String etiquetaTipus;
    private final Color color;
    private final int amplePx;
    private final int altPx;

    IconaMarcador(byte[] icona, String etiquetaTipus, Color color, int amplePx, int altPx) {
        this.icona = icona.clone();      // copia defensiva: ningu no muta allo compartit
        this.etiquetaTipus = etiquetaTipus;
        this.color = color;
        this.amplePx = amplePx;
        this.altPx = altPx;
    }

    /** L'estat extrinsec (posicio) arriba per parametres a CADA crida. */
    public void dibuixar(Llenc llenc, double latitud, double longitud) {
        llenc.pintarImatge(icona, latitud, longitud, amplePx, altPx, color);
    }

    public String getEtiquetaTipus() { return etiquetaTipus; }
}

La fàbrica de flyweights — el guardià de la compartició; fixa't que el constructor d'IconaMarcador és de visibilitat de paquet: només la fàbrica crea flyweights:

public class FabricaIcones {

    /** La clau identifica la combinacio intrinseca completa. */
    private record ClauIcona(TipusEntitat tipus, Estat estat) {}

    private final Map<ClauIcona, IconaMarcador> cache = new ConcurrentHashMap<>();
    private final CarregadorRecursos recursos;

    public FabricaIcones(CarregadorRecursos recursos) { this.recursos = recursos; }

    public IconaMarcador obtenir(TipusEntitat tipus, Estat estat) {
        return cache.computeIfAbsent(new ClauIcona(tipus, estat), clau ->
                new IconaMarcador(
                        recursos.carregarPng(clau.tipus()),      // els 32 KB, nomes el 1r cop
                        clau.tipus().etiqueta(),
                        clau.estat().color(),
                        48, 48));
    }

    public int midaCache() { return cache.size(); }
}

El context i el client — milers de marcadors lleugers apuntant a desenes d'icones compartides:

public class MarcadorMapa {
    private final String id;
    private volatile double latitud, longitud;   // extrinsec: canvia amb cada GPS
    private final IconaMarcador icona;           // referencia COMPARTIDA

    public MarcadorMapa(String id, double lat, double lon, IconaMarcador icona) {
        this.id = id; this.latitud = lat; this.longitud = lon; this.icona = icona;
    }

    public void moureA(double lat, double lon) { this.latitud = lat; this.longitud = lon; }

    public void dibuixar(Llenc llenc) {
        icona.dibuixar(llenc, latitud, longitud);   // el context aporta l'extrinsec
    }
}
FabricaIcones fabrica = new FabricaIcones(recursos);
List<MarcadorMapa> marcadors = new ArrayList<>();

for (RepartidorEnViu r : flotaEnViu()) {            // 4.000 repartidors
    marcadors.add(new MarcadorMapa(
            r.getId(), r.getLatitud(), r.getLongitud(),
            fabrica.obtenir(r.getTipusVehicle().tipusEntitat(), r.getEstat())));
}
for (Restaurant rest : restaurantsOberts()) {       // 9.000 restaurants
    marcadors.add(new MarcadorMapa(
            rest.getId(), rest.getLatitud(), rest.getLongitud(),
            fabrica.obtenir(rest.getCategoria().tipusEntitat(), Estat.OBERT)));
}

marcadors.forEach(m -> m.dibuixar(llenc));
System.out.println("Flyweights creats: " + fabrica.midaCache());   // ~25, no 13.000

El compte després del patró: ~25 flyweights × 32 KB ≈ 0,8 MB d'icones (abans: 400 MB), més 13.000 contextos de ~50 bytes. Dos ordres de magnitud, sense tocar el renderitzat. La fàbrica —cosina germana de les del mòdul 2, amb memòria— és la que converteix la bona intenció en garantia: ningú no pot crear una icona duplicada perquè ningú més no pot crear icones.

Flyweight al JDK: Integer.valueOf i l'string pool

Java practica aquest patró des de sempre, en dos llocs que ja has usat:

  • Integer.valueOf(int) (i l'autoboxing, que el crida per sota) manté una cache dels enters de −128 a 127: Integer.valueOf(42) == Integer.valueOf(42) és true — mateixa instància compartida —, mentre que valueOf(1000) == valueOf(1000) és false. És una FabricaIcones d'enters: els valors més freqüents, compartits; la resta, creats. (I la moralitat de sempre: els Integer es comparen amb equals, mai amb ==, precisament perquè la compartició és un detall intern.)
  • L'string pool: els literals de String s'internen i comparteixen ("pizza" == "pizza" és true; new String("pizza") crea una altra instància fora del pool, i intern() la retorna al ramat). Possible gràcies al fet que String és immutable — la mateixa condició que exigim a IconaMarcador. Compartició massiva d'objectes de gra fi: Flyweight de llibre.

A la mateixa família: Boolean.valueOf, Character.valueOf (ASCII), els enum (cada constant és una instància única compartida). Quan un valueOf estàtic et retorni "l'" objecte en lloc d'"un" objecte, sospita flyweight.

Quan NO val la pena

Flyweight és el patró amb el llindar d'entrada més alt del mòdul, i convé dir-ho sense embuts. No l'apliquis quan:

  • No hi ha multitud. Amb centenars d'objectes —fins i tot pocs milers de petits— la JVM ni s'immuta. El patró comença a pagar amb desenes de milers d'instàncies i estat repetit pesant. Abans d'aplicar-lo, mesura (un profiler, Runtime.totalMemory(), un heap dump): l'optimització sense mesurament és superstició, i aquest patró és, abans de res, una optimització.
  • L'estat gairebé no es repeteix. Si cada marcador tingués una icona personalitzada, no hi hauria res per compartir: la cache de la fàbrica creixeria fins a contenir un flyweight per objecte — tot el cost, cap renda.
  • L'estat "intrínsec" muta. Si el color de la icona ha de parpellejar per marcador individual, aquella dada era extrínseca per molt que semblés de tipus. Compartir estat mutable no és Flyweight: és una fàbrica de heisenbugs entre fils.
  • El preu en claredat no compensa. Separar intrínsec/extrínsec complica firmes (tot viatja per paràmetres) i raonament. Per al 95% del codi de PideYa —comandes, cistelles, notificadors—, aquest preu no compraria res: els objectes es compten d'un en un. La balança de la lliçó 01-06, una vegada més.

Quan sí, en positiu: multituds (>10⁴) d'objectes de gra fi, amb una part substancial del seu estat repetida, immutable o immutabilitzable, i pressió de memòria mesurada. Mapes, editors (un flyweight per caràcter/estil, l'exemple GoF original), jocs (partícules, tiles), caches de glifs i sprites.

Relació amb altres patrons (només menció): la FabricaIcones és una fàbrica del mòdul 2 amb cache — i sovint instància única (Singleton o injectada); les fulles compartides d'un Composite enorme poden ser flyweights; Prototype és en certa manera el seu mirall: clonar perquè cadascú tingui la seva còpia davant de compartir perquè ningú no tingui la seva; i no confonguis la cache de flyweights (comparteix objectes immutables per estalviar memòria) amb un Proxy de cache (evita crides cares recordant resultats), que veurem tot seguit.

Errors Comuns i Consells

  • Flyweight mutable. L'error letal. Algú afegeix setColor(...) a IconaMarcador "per ressaltar un repartidor"... i ressalta els 800 que comparteixen instància. Tot flyweight: final, camps final, còpies defensives i cap setter. El compilador és el teu guardià si el deixes.
  • Colar estat extrínsec a dins. "Ja que hi som, que la icona desi l'última posició dibuixada": adéu compartició (cada marcador necessitaria la seva) o adéu correcció (tots trepitjant-se la dada). La disciplina de la frontera intrínsec/extrínsec és el patró sencer.
  • Fàbrica amb fuites. La cache reté els flyweights per sempre; si les claus són il·limitades (un flyweight per restaurant en lloc de per categoria?), l'"optimització de memòria" es converteix en fuita de memòria. Claus de cardinalitat acotada i coneguda; si no ho són, no era estat intrínsec.
  • Saltar-se la fàbrica. Un new IconaMarcador(...) fora de la fàbrica trenca la garantia d'unicitat en silenci. Tanca-ho per construcció: constructor de paquet (com hem fet) o niat a la fàbrica.
  • Aplicar-lo per si de cas. Dissenyar el mapa "ja amb flyweight" quan el pilot té 40 repartidors és optimització prematura de manual. Primer el model simple; el patró, quan el profiler ho demani (refactoritzar cap a ell és mecànic: extreure camps repetits, crear la fàbrica, substituir).
  • Consell: exposa midaCache() (o mètriques equivalents) a la fàbrica des del primer dia. Un número que hauria de rondar els 25 i marca 11.000 és el detector de fum de gairebé tots els errors anteriors.

Exercicis

Exercici 1: separar el gra

L'historial de comandes de PideYa manté en memòria, per a anàlisi en viu, un objecte per cada línia de comanda del dia (uns 2 milions): nomPlat (String), preuUnitari (BigDecimal), categoriaFiscal (String), urlFoto (String), quantitat (int), horaComanda (LocalTime), idComanda (long). Classifica cada camp com a intrínsec o extrínsec i esbossa el flyweight resultant i la seva clau de fàbrica.

Exercici 2: el bug del ressaltat

Operacions demana ressaltar en groc el marcador del repartidor seleccionat. Un company ho implementa afegint setRessaltat(boolean) a IconaMarcador. Explica el bug exacte que veurà el panell i dóna dues solucions compatibles amb el patró.

Exercici 3: flyweight o no?

Decideix amb criteri (multitud, repetició, immutabilitat, mesurament) si aplicaries Flyweight: (1) els 25 objectes EstatComanda possibles referenciats per milions de comandes; (2) les 300 fotos de plats, totes diferents, mostrades a la carta d'un restaurant; (3) els objectes ZonaRepartiment (polígon de ~200 vèrtexs) compartits pels ~150 repartidors de cada zona.

Solucions

Solució 1: intrínsecs (es repeteixen en massa, no varien per aparició): nomPlat, preuUnitari, categoriaFiscal, urlFoto — junts formen un flyweight ProducteCataleg; la carta de PideYa té ~50.000 plats diferents davant de 2M de línies: compartició 40:1. Extrínsecs (propis de cada línia): quantitat, horaComanda, idComanda. Resultat: record LiniaAnalisi(long idComanda, int quantitat, LocalTime hora, ProducteCataleg producte), amb FabricaProductesCataleg fent cache per clau idPlat (o nom+restaurant). Matís honest: si el preu d'un plat canvia durant el dia, el preu en el moment de la comanda és extrínsec (històric), no intrínsec — la frontera la fixa la semàntica, no el tipus de dada.

Solució 2: el bug: en fer icona.setRessaltat(true) sobre el flyweight "moto-repartint", tots els repartidors en moto repartint es tornen grocs alhora (comparteixen la instància); en desseleccionar, tots s'apaguen. Solucions: (a) el ressaltat és estat extrínsec: el panell desa idSeleccionat i el passa en dibuixar (icona.dibuixar(llenc, lat, lon, ressaltat)), amb la qual cosa el flyweight continua immutable; (b) tractar "ressaltat" com a part de la clau intrínseca: combinacions (tipus, estat, ressaltat) a la fàbrica — duplica les combinacions (continua sent ~50) i el marcador seleccionat obté un altre flyweight. La (a) és preferible: el ressaltat és context de la sessió del panell, no identitat del tipus.

Solució 3: (1) Ja és flyweight sense dir-ne així: 25 instàncies immutables compartides per milions — implementació natural: un enum EstatComanda, que és la forma idiomàtica Java del patró per a catàlegs tancats. (2) No: 300 objectes tots diferents = res de repetit per compartir; el seu problema (no carregar 300 fotos de cop) és de càrrega mandrosa, que és exactament el proxy virtual de la pròxima lliçó. (3) Sí, i gairebé sense cerimònia: polígons pesants (~200 vèrtexs), repetició 150:1, geometria immutable; n'hi ha prou que els repartidors rebin la referència a la ZonaRepartiment compartida en lloc de copiar-la — flyweight sense fàbrica elaborada, només disciplina de compartir. Fixa't que (3) mostra que el patró de vegades és només "deixar de copiar".

Conclusió

Flyweight ataca la multiplicació: separa l'estat intrínsec (repetit, immutable) de l'extrínsec (contextual, canviant), comparteix el primer mitjançant una fàbrica amb memòria i fa viatjar el segon per paràmetres. A PideYa va deixar el mapa en temps real a dieta —de 400 MB a menys d'un, amb IconaMarcador i FabricaIcones— i de passada t'ha ensenyat a llegir Integer.valueOf i l'string pool com el que són. I va quedar dit amb claredat quan no usar-lo: sense multitud mesurada, sense repetició i sense immutabilitat, aquest patró només afegeix fricció.

Queda un últim patró estructural, i tanca el cercle dels embolcalls: un objecte que es fa passar per un altre —mateixa interfície, com Decorator— però no per afegir-li responsabilitats, sinó per controlar l'accés: retardar una càrrega costosa fins que calgui, comprovar permisos abans de deixar passar, recordar respostes per no repetir viatges. A PideYa l'esperen les fotos dels plats (que avui es carreguen totes de cop) i les operacions sensibles del panell d'administració. Ens veiem a Proxy.

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