Hasta aquí, los estructurales han organizado piezas contadas: un adaptador, dos jerarquías, un árbol de carta, unas capas de extras, una fachada. Flyweight entra en escena cuando las piezas se cuentan por decenas de miles y la memoria empieza a protestar. Es el patrón más especializado del módulo —el que menos veces aplicarás— y a la vez uno que usas a diario sin saberlo, porque el propio Java lo lleva de serie. Su idea cabe en una frase: si diez mil objetos repiten los mismos datos, que los diez mil compartan una sola copia de lo repetido y carguen cada uno solo con lo que de verdad les es propio.

Contenido

  1. El problema en PideYa: el mapa en tiempo real
  2. Estado intrínseco y estado extrínseco
  3. Intención y estructura del patrón
  4. Implementación Java completa
  5. Flyweight en el JDK: Integer.valueOf y el string pool
  6. Cuándo NO merece la pena
  7. Errores comunes, ejercicios y conclusión

El problema en PideYa: el mapa en tiempo real

El centro de operaciones de PideYa muestra un mapa de la ciudad con todos los repartidores y restaurantes en vivo: en Madrid en hora punta, unos 4.000 repartidores (en bici, en moto, en coche, cada tipo con su icono y su color según estado: disponible, repartiendo, en pausa) y 9.000 restaurantes (icono por categoría: pizzería, sushi, hamburguesas...). Cada marcador se refresca varias veces por segundo. El primer modelo, uno a uno:

// Primer intento: cada marcador carga con TODO. NO imitar.
public class MarcadorMapa {
    // Lo que cambia por marcador:
    private String id;                 // "repartidor-8841"
    private double latitud;
    private double longitud;

    // Lo que se repite en miles de marcadores idénticos:
    private byte[] icono;              // PNG del icono... ¡32 KB!
    private String etiquetaTipo;       // "Repartidor en moto"
    private Color color;               // según estado
    private int anchoPx, altoPx;       // tamaño de renderizado
}

La cuenta es demoledora: 13.000 marcadores × ~32 KB de icono ≈ 400 MB solo en iconos... cuando en realidad solo existen unas decenas de combinaciones distintas de (icono, etiqueta, color, tamaño): moto-disponible, moto-repartiendo, bici-disponible, pizzería, sushi... Todo lo demás es la misma información duplicada miles de veces. En la app del cliente (móviles modestos) el mapa directamente no cabe; en el panel de operaciones, el recolector de basura no levanta cabeza.

El despilfarro tiene una estructura precisa: cada marcador mezcla dos clases de estado con vidas completamente distintas. Separarlas es todo el patrón.

Estado intrínseco y estado extrínseco

La distinción central de Flyweight, que conviene dominar antes de ver una línea de código:

Estado intrínseco Estado extrínseco
Qué es Lo que el objeto es por su tipo: igual en todos los ejemplares de esa clase de marcador Lo que el objeto es por su contexto: distinto en cada aparición
En el mapa Icono, etiqueta de tipo, color de estado, tamaño Id del repartidor/restaurante, latitud, longitud
¿Depende del contexto? No: "moto-disponible" es igual aquí y en Vallecas Sí: cada marcador tiene la suya
¿Mutable? Jamás: debe ser inmutable para compartirse Libremente: cambia con cada refresco de GPS
Dónde vive Dentro del flyweight, una sola copia compartida Fuera: lo aporta el cliente en cada llamada
Cuántos hay Decenas (combinaciones de tipo × estado) Miles (uno por entidad en el mapa)

La receta del patrón: extraer el estado intrínseco a objetos compartidos e inmutables (los flyweights), sacar el extrínseco fuera (el cliente lo pasa como parámetros en cada operación), y poner una fábrica que garantice que cada combinación intrínseca se crea una sola vez.

Intención y estructura del patrón

Intención (GoF): usar compartición para dar soporte eficiente a grandes cantidades de objetos de grano fino.

classDiagram
    class IconoMarcador {
        <<flyweight>>
        -icono: byte[]
        -etiquetaTipo: String
        -color: Color
        -anchoPx: int
        -altoPx: int
        +dibujar(lienzo: Lienzo, latitud: double, longitud: double)
    }
    class FabricaIconos {
        -cache: Map~ClaveIcono, IconoMarcador~
        +obtener(tipo: TipoEntidad, estado: Estado) IconoMarcador
        +tamanioCache() int
    }
    class MarcadorMapa {
        -id: String
        -latitud: double
        -longitud: double
        -icono: IconoMarcador
        +dibujar(lienzo: Lienzo)
    }
    class PanelOperaciones

    FabricaIconos o-- IconoMarcador : cachea y comparte
    MarcadorMapa --> IconoMarcador : referencia compartida
    PanelOperaciones --> FabricaIconos : obtiene flyweights
    PanelOperaciones --> MarcadorMapa : mantiene miles
Rol GoF En PideYa
Flyweight (estado intrínseco compartido, inmutable) IconoMarcador
FlyweightFactory (crea o reutiliza, cachea) FabricaIconos
Client / contexto (guarda el estado extrínseco) MarcadorMapa (id, posición) y el panel

Dos observaciones sobre el diagrama:

  • La operación del flyweight (dibujar) recibe el estado extrínseco por parámetros: el flyweight no sabe dónde está, se lo dicen en cada llamada. Esa firma es el sello del patrón.
  • El objeto de contexto (MarcadorMapa) queda reducido a lo mínimo: id, dos doubles y una referencia al flyweight compartido. De 32 KB a unas decenas de bytes por marcador.

Implementación Java completa

El flyweight — inmutable a conciencia, condición innegociable para compartirlo (incluso entre hilos, gratis):

public final class IconoMarcador {

    private final byte[] icono;          // los 32 KB, UNA vez por combinación
    private final String etiquetaTipo;
    private final Color color;
    private final int anchoPx;
    private final int altoPx;

    IconoMarcador(byte[] icono, String etiquetaTipo, Color color, int anchoPx, int altoPx) {
        this.icono = icono.clone();      // copia defensiva: nadie muta lo compartido
        this.etiquetaTipo = etiquetaTipo;
        this.color = color;
        this.anchoPx = anchoPx;
        this.altoPx = altoPx;
    }

    /** El estado extrínseco (posición) llega por parámetros en CADA llamada. */
    public void dibujar(Lienzo lienzo, double latitud, double longitud) {
        lienzo.pintarImagen(icono, latitud, longitud, anchoPx, altoPx, color);
    }

    public String getEtiquetaTipo() { return etiquetaTipo; }
}

La fábrica de flyweights — el guardián de la compartición; fíjate en que el constructor de IconoMarcador es de visibilidad de paquete: solo la fábrica crea flyweights:

public class FabricaIconos {

    /** La clave identifica la combinación intrínseca completa. */
    private record ClaveIcono(TipoEntidad tipo, Estado estado) {}

    private final Map<ClaveIcono, IconoMarcador> cache = new ConcurrentHashMap<>();
    private final CargadorRecursos recursos;

    public FabricaIconos(CargadorRecursos recursos) { this.recursos = recursos; }

    public IconoMarcador obtener(TipoEntidad tipo, Estado estado) {
        return cache.computeIfAbsent(new ClaveIcono(tipo, estado), clave ->
                new IconoMarcador(
                        recursos.cargarPng(clave.tipo()),        // los 32 KB, solo la 1ª vez
                        clave.tipo().etiqueta(),
                        clave.estado().color(),
                        48, 48));
    }

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

El contexto y el cliente — miles de marcadores ligeros apuntando a decenas de iconos compartidos:

public class MarcadorMapa {
    private final String id;
    private volatile double latitud, longitud;   // extrínseco: cambia con cada GPS
    private final IconoMarcador icono;           // referencia COMPARTIDA

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

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

    public void dibujar(Lienzo lienzo) {
        icono.dibujar(lienzo, latitud, longitud);   // el contexto aporta lo extrínseco
    }
}
FabricaIconos fabrica = new FabricaIconos(recursos);
List<MarcadorMapa> marcadores = new ArrayList<>();

for (RepartidorEnVivo r : flotaEnVivo()) {          // 4.000 repartidores
    marcadores.add(new MarcadorMapa(
            r.getId(), r.getLatitud(), r.getLongitud(),
            fabrica.obtener(r.getTipoVehiculo().tipoEntidad(), r.getEstado())));
}
for (Restaurante rest : restaurantesAbiertos()) {   // 9.000 restaurantes
    marcadores.add(new MarcadorMapa(
            rest.getId(), rest.getLatitud(), rest.getLongitud(),
            fabrica.obtener(rest.getCategoria().tipoEntidad(), Estado.ABIERTO)));
}

marcadores.forEach(m -> m.dibujar(lienzo));
System.out.println("Flyweights creados: " + fabrica.tamanioCache());   // ~25, no 13.000

La cuenta después del patrón: ~25 flyweights × 32 KB ≈ 0,8 MB de iconos (antes: 400 MB), más 13.000 contextos de ~50 bytes. Dos órdenes de magnitud, sin tocar el renderizado. La fábrica —prima hermana de las del módulo 2, con memoria— es la que convierte la buena intención en garantía: nadie puede crear un icono duplicado porque nadie más puede crear iconos.

Flyweight en el JDK: Integer.valueOf y el string pool

Java practica este patrón desde siempre, en dos sitios que ya has usado:

  • Integer.valueOf(int) (y el autoboxing, que lo llama por debajo) mantiene una caché de los enteros de −128 a 127: Integer.valueOf(42) == Integer.valueOf(42) es true — misma instancia compartida —, mientras que valueOf(1000) == valueOf(1000) es false. Es una FabricaIconos de enteros: los valores más frecuentes, compartidos; el resto, creados. (Y la moraleja de siempre: los Integer se comparan con equals, nunca con ==, precisamente porque la compartición es un detalle interno.)
  • El string pool: los literales de String se internan y comparten ("pizza" == "pizza" es true; new String("pizza") crea otra instancia fuera del pool, y intern() la devuelve al redil). Posible gracias a que String es inmutable — la misma condición que exigimos a IconoMarcador. Compartición masiva de objetos de grano fino: Flyweight de libro.

En la misma familia: Boolean.valueOf, Character.valueOf (ASCII), los enum (cada constante es una instancia única compartida). Cuando un valueOf estático te devuelve "el" objeto en lugar de "un" objeto, sospecha flyweight.

Cuándo NO merece la pena

Flyweight es el patrón con el umbral de entrada más alto del módulo, y conviene decirlo sin rodeos. No lo apliques cuando:

  • No hay multitud. Con cientos de objetos —incluso pocos miles pequeños— la JVM ni se inmuta. El patrón empieza a pagar con decenas de miles de instancias y estado repetido pesado. Antes de aplicarlo, mide (un profiler, Runtime.totalMemory(), un heap dump): la optimización sin medición es superstición, y este patrón es, ante todo, una optimización.
  • El estado apenas se repite. Si cada marcador tuviera un icono personalizado, no habría nada que compartir: la caché de la fábrica crecería hasta contener un flyweight por objeto — todo el coste, ninguna renta.
  • El estado "intrínseco" muta. Si el color del icono debe parpadear por marcador individual, ese dato era extrínseco por mucho que pareciera de tipo. Compartir estado mutable no es Flyweight: es una fábrica de heisenbugs entre hilos.
  • El precio en claridad no compensa. Separar intrínseco/extrínseco complica firmas (todo viaja por parámetros) y razonamiento. Para el 95% del código de PideYa —pedidos, carritos, notificadores—, ese precio compraría nada: los objetos se cuentan de uno en uno. La balanza de la lección 01-06, una vez más.

Cuándo sí, en positivo: multitudes (>10⁴) de objetos de grano fino, con una parte sustancial de su estado repetida, inmutable o inmutabilizable, y presión de memoria medida. Mapas, editores (un flyweight por carácter/estilo, el ejemplo GoF original), juegos (partículas, tiles), cachés de glifos y sprites.

Relación con otros patrones (solo mención): la FabricaIconos es una fábrica del módulo 2 con caché — y a menudo instancia única (Singleton o inyectada); las hojas compartidas de un Composite enorme pueden ser flyweights; Prototype es en cierto modo su espejo: clonar para que cada cual tenga su copia frente a compartir para que nadie tenga la suya; y no confundas la caché de flyweights (comparte objetos inmutables para ahorrar memoria) con un Proxy de caché (evita llamadas caras recordando resultados), que veremos a continuación.

Errores Comunes y Consejos

  • Flyweight mutable. El error letal. Alguien añade setColor(...) a IconoMarcador "para resaltar un repartidor"... y resalta los 800 que comparten instancia. Todo flyweight: final, campos final, copias defensivas y ningún setter. El compilador es tu guardián si le dejas.
  • Colar estado extrínseco dentro. "Ya que estamos, que el icono guarde la última posición dibujada": adiós compartición (cada marcador necesitaría el suyo) o adiós corrección (todos pisándose el dato). La disciplina de la frontera intrínseco/extrínseco es el patrón entero.
  • Fábrica con fugas. La caché retiene los flyweights para siempre; si las claves son ilimitadas (¿un flyweight por restaurante en vez de por categoría?), la "optimización de memoria" se convierte en fuga de memoria. Claves de cardinalidad acotada y conocida; si no lo son, no era estado intrínseco.
  • Saltarse la fábrica. Un new IconoMarcador(...) fuera de la fábrica rompe la garantía de unicidad en silencio. Ciérralo por construcción: constructor de paquete (como hicimos) o anidado en la fábrica.
  • Aplicarlo por si acaso. Diseñar el mapa "ya con flyweight" cuando el piloto tiene 40 repartidores es optimización prematura de manual. Primero el modelo simple; el patrón, cuando el profiler lo pida (refactorizar hacia él es mecánico: extraer campos repetidos, crear la fábrica, sustituir).
  • Consejo: expón tamanioCache() (o métricas equivalentes) en la fábrica desde el primer día. Un número que debería rondar 25 y marca 11.000 es el detector de humo de casi todos los errores anteriores.

Ejercicios

Ejercicio 1: separar el grano

El historial de pedidos de PideYa mantiene en memoria, para análisis en vivo, un objeto por cada línea de pedido del día (unos 2 millones): nombrePlato (String), precioUnitario (BigDecimal), categoriaFiscal (String), urlFoto (String), cantidad (int), horaPedido (LocalTime), idPedido (long). Clasifica cada campo como intrínseco o extrínseco y esboza el flyweight resultante y su clave de fábrica.

Ejercicio 2: el bug del resaltado

Operaciones pide resaltar en amarillo el marcador del repartidor seleccionado. Un compañero lo implementa añadiendo setResaltado(boolean) a IconoMarcador. Explica el bug exacto que verá el panel y da dos soluciones compatibles con el patrón.

Ejercicio 3: ¿flyweight o no?

Decide con criterio (multitud, repetición, inmutabilidad, medición) si aplicarías Flyweight: (1) los 25 objetos EstadoPedido posibles referenciados por millones de pedidos; (2) las 300 fotos de platos, todas distintas, mostradas en la carta de un restaurante; (3) los objetos ZonaReparto (polígono de ~200 vértices) compartidos por los ~150 repartidores de cada zona.

Soluciones

Solución 1: intrínsecos (se repiten en masa, no varían por aparición): nombrePlato, precioUnitario, categoriaFiscal, urlFoto — juntos forman un flyweight ProductoCatalogo; la carta de PideYa tiene ~50.000 platos distintos frente a 2M de líneas: compartición 40:1. Extrínsecos (propios de cada línea): cantidad, horaPedido, idPedido. Resultado: record LineaAnalisis(long idPedido, int cantidad, LocalTime hora, ProductoCatalogo producto), con FabricaProductosCatalogo cacheando por clave idPlato (o nombre+restaurante). Matiz honesto: si el precio de un plato cambia durante el día, el precio en el momento del pedido es extrínseco (histórico), no intrínseco — la frontera la fija la semántica, no el tipo de dato.

Solución 2: el bug: al hacer icono.setResaltado(true) sobre el flyweight "moto-repartiendo", todos los repartidores en moto repartiendo se vuelven amarillos a la vez (comparten la instancia); al deseleccionar, todos se apagan. Soluciones: (a) el resaltado es estado extrínseco: el panel guarda idSeleccionado y lo pasa al dibujar (icono.dibujar(lienzo, lat, lon, resaltado)), con lo que el flyweight sigue inmutable; (b) tratar "resaltado" como parte de la clave intrínseca: combinaciones (tipo, estado, resaltado) en la fábrica — duplica las combinaciones (sigue siendo ~50) y el marcador seleccionado obtiene otro flyweight. La (a) es preferible: el resaltado es contexto de la sesión del panel, no identidad del tipo.

Solución 3: (1) Ya es flyweight sin llamarlo así: 25 instancias inmutables compartidas por millones — implementación natural: un enum EstadoPedido, que es la forma idiomática Java del patrón para catálogos cerrados. (2) No: 300 objetos todos distintos = nada repetido que compartir; su problema (no cargar 300 fotos de golpe) es de carga perezosa, que es exactamente el proxy virtual de la próxima lección. (3) Sí, y casi sin ceremonia: polígonos pesados (~200 vértices), repetición 150:1, geometría inmutable; basta con que los repartidores reciban la referencia a la ZonaReparto compartida en vez de copiarla — flyweight sin fábrica elaborada, solo disciplina de compartir. Nótese que (3) muestra que el patrón a veces es solo "dejar de copiar".

Conclusión

Flyweight ataca la multiplicación: separa el estado intrínseco (repetido, inmutable) del extrínseco (contextual, cambiante), comparte el primero mediante una fábrica con memoria y hace viajar el segundo por parámetros. En PideYa dejó el mapa en tiempo real a dieta —de 400 MB a menos de uno, con IconoMarcador y FabricaIconos— y de paso te enseñó a leer Integer.valueOf y el string pool como lo que son. Y quedó dicho con claridad cuándo no usarlo: sin multitud medida, sin repetición y sin inmutabilidad, este patrón solo añade fricción.

Queda un último patrón estructural, y cierra el círculo de los envoltorios: un objeto que se hace pasar por otro —misma interfaz, como Decorator— pero no para añadirle responsabilidades, sino para controlar el acceso: retrasar una carga costosa hasta que haga falta, comprobar permisos antes de dejar pasar, recordar respuestas para no repetir viajes. En PideYa lo esperan las fotos de los platos (que hoy se cargan todas de golpe) y las operaciones sensibles del panel de administración. Nos vemos en Proxy.

Curso de Patrones de Diseño de Software

Módulo 1: Introducción a los Patrones de Diseño

Módulo 2: Patrones Creacionales

Módulo 3: Patrones Estructurales

Módulo 4: Patrones de Comportamiento

Módulo 5: Aplicación de Patrones de Diseño

Módulo 6: Patrones de Diseño Avanzados

Módulo 7: Recursos Adicionales y Conclusión

© Copyright 2026. Todos los derechos reservados