17. Flyweight (Peso Ligero) - Patrón Estructural

El patrón Flyweight propone compartir objetos pequeños que representan información intrínseca para reducir el consumo de memoria cuando existen cantidades masivas de entidades similares. Los datos extrínsecos, que varían entre instancias lógicas, se mantienen fuera del objeto compartido y se pasan en cada operación.

Esta estrategia permite generar estructuras millonarias sin multiplicar la memoria requerida. El cliente percibe que maneja objetos independientes, pero en realidad se reutiliza un subconjunto limitado de flyweights que encapsulan el estado inmutable y pesado.

17.1 Problema y contexto

Aplicaciones como editores de texto, videojuegos, motores de mapas o sistemas financieros manejan colecciones gigantes de elementos con atributos repetidos: letras, celdas, partículas, fichas. Instanciar objetos completos para cada elemento provoca consumo excesivo de memoria, fatiga al recolector de basura y eventualmente errores de asignación.

Flyweight aparece cuando se detecta que una parte del estado es compartible e inmutable. Separar ese estado y compartirlo permite que cada entidad lógica conserve solo la información variable (extrínseca), logrando un uso eficiente de recursos sin sacrificar flexibilidad.

17.2 Intención y motivación

La intención del patrón es usar compartición para soportar grandes cantidades de objetos de forma eficiente. Se justifica cuando:

  • El sistema necesita millones de instancias que podrían reutilizar la mayor parte de sus datos.
  • Los objetos comparten estado inmutable que se puede factorizar en una representación común.
  • El costo de identificar un flyweight existente es menor que crear una instancia nueva.
  • Se requiere disminuir la presión de memoria o los tiempos de carga sin cambiar la interfaz pública.

El patrón motiva a descomponer cada entidad en componentes intrínsecos (compartibles) y extrínsecos (propios de cada uso). De esta forma se mantiene la ilusión de objetos independientes delegando la variabilidad al contexto que invoca.

17.3 Participantes y estructura

La estructura clásica del patrón identifica los siguientes roles:

  • Flyweight: interfaz que declara operaciones que reciben estado extrínseco.
  • ConcreteFlyweight: implementación que almacena el estado intrínseco compartido.
  • FlyweightFactory: responsable de crear y gestionar la reutilización de objetos, devolviendo instancias existentes cuando sea posible.
  • UnsharedConcreteFlyweight: opcional, representa elementos que no se comparten pero interactúan con el resto.
  • Cliente: mantiene o calcula el estado extrínseco necesario para invocar el flyweight.

La fábrica suele utilizar estructuras como mapas o caches para devolver la misma instancia cuando el estado intrínseco coincide. El cliente decide cómo administrar los datos extrínsecos: almacenarlos en objetos de contexto, calcularlos al vuelo o derivarlos de otras colecciones.

17.4 Clasificación del estado: intrínseco vs. extrínseco

La clave del patrón es diferenciar correctamente los tipos de estado:

  • Estado intrínseco: inmutable, compartible y perteneciente al flyweight. No depende del contexto de uso.
  • Estado extrínseco: varía entre representaciones lógicas y el cliente lo pasa cuando invoca las operaciones.

Una mala clasificación genera inconsistencias o la necesidad de replicar flyweights. Por ejemplo, en un editor de texto el glifo comparte la forma de la letra (intrínseco) y recibe la posición, el estilo y el resaltado que cambian por cada aparición (extrínseco).

17.5 Escenario: motor tipográfico de un editor en la nube

Imaginemos un editor colaborativo que debe renderizar rápidamente documentos con cientos de miles de caracteres. Instanciar un objeto completo por cada letra saturaría la memoria del navegador. Se necesita compartir la información intrínseca (carácter, metadatos de trazado, ancho base) y mantener de forma separada el estado extrínseco (posición, formato, resaltado, usuario que lo editó).

Flyweight permite construir una fábrica que devuelve el flyweight correspondiente al carácter y delega en estructuras auxiliares el contexto variable. El resultado es un editor escalable que ahorra memoria, acelera el renderizado y reduce la latencia en sesiones colaborativas.

17.6 Implementación en Java

El siguiente ejemplo modela el escenario descrito con una fábrica que comparte glifos y objetos de contexto que aportan la información extrínseca:

package tutorial.flyweight;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public interface CaracterFlyweight {
    void dibujar(ContextoCaracter contexto);
}

class CaracterConcreto implements CaracterFlyweight {
    private final char simbolo;
    private final int codigoUnicode;
    private final boolean soportaLigadura;

    CaracterConcreto(char simbolo, boolean soportaLigadura) {
        this.simbolo = simbolo;
        this.codigoUnicode = simbolo;
        this.soportaLigadura = soportaLigadura;
    }

    @Override
    public void dibujar(ContextoCaracter contexto) {
        System.out.printf("Render '%s' (%d) en [%d,%d] fuente=%s tamaño=%d color=%s resaltado=%s usuario=%s%n",
                simbolo,
                codigoUnicode,
                contexto.posicion().x(),
                contexto.posicion().y(),
                contexto.formato().fuente(),
                contexto.formato().tamano(),
                contexto.formato().colorHex(),
                contexto.resaltado(),
                contexto.usuario());
        if (soportaLigadura && contexto.formato().activarLigaduras()) {
            System.out.println("  > Aplicando ligaduras tipográficas");
        }
    }
}

record ContextoCaracter(Posicion posicion,
                        FormatoCaracter formato,
                        boolean resaltado,
                        String usuario) {
}

record Posicion(int x, int y) {
}

class FormatoCaracter {
    private final String fuente;
    private final int tamano;
    private final String colorHex;
    private final boolean activarLigaduras;

    FormatoCaracter(String fuente, int tamano, String colorHex, boolean activarLigaduras) {
        this.fuente = Objects.requireNonNull(fuente);
        this.tamano = tamano;
        this.colorHex = Objects.requireNonNull(colorHex);
        this.activarLigaduras = activarLigaduras;
    }

    public String fuente() {
        return fuente;
    }

    public int tamano() {
        return tamano;
    }

    public String colorHex() {
        return colorHex;
    }

    public boolean activarLigaduras() {
        return activarLigaduras;
    }
}

class FabricaCaracteres {
    private final Map<Character, CaracterFlyweight> cache = new HashMap<>();

    public CaracterFlyweight obtener(char simbolo) {
        return cache.computeIfAbsent(simbolo, this::crearFlyweight);
    }

    private CaracterFlyweight crearFlyweight(char simbolo) {
        boolean soportaLigadura = switch (simbolo) {
            case 'f', 'i', 'l' -> true;
            default -> false;
        };
        return new CaracterConcreto(simbolo, soportaLigadura);
    }

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

class DocumentoColaborativo {
    private final FabricaCaracteres fabricaCaracteres;
    private final List<ContextoCaracter> contextos = new ArrayList<>();

    DocumentoColaborativo(FabricaCaracteres fabricaCaracteres) {
        this.fabricaCaracteres = fabricaCaracteres;
    }

    public void agregarCaracter(char simbolo, ContextoCaracter contexto) {
        CaracterFlyweight flyweight = fabricaCaracteres.obtener(simbolo);
        flyweight.dibujar(contexto);
        contextos.add(contexto);
    }

    public List<ContextoCaracter> contextos() {
        return Collections.unmodifiableList(contextos);
    }
}

class AplicacionFlyweight {
    public static void main(String[] args) {
        FabricaCaracteres fabrica = new FabricaCaracteres();
        DocumentoColaborativo documento = new DocumentoColaborativo(fabrica);

        FormatoCaracter formatoTitulo = new FormatoCaracter("Inter", 18, "#222222", true);
        FormatoCaracter formatoParrafo = new FormatoCaracter("Inter", 12, "#333333", false);

        documento.agregarCaracter('F', new ContextoCaracter(new Posicion(0, 0), formatoTitulo, true, "Ana"));
        documento.agregarCaracter('l', new ContextoCaracter(new Posicion(1, 0), formatoTitulo, true, "Ana"));
        documento.agregarCaracter('y', new ContextoCaracter(new Posicion(2, 0), formatoTitulo, true, "Ana"));
        documento.agregarCaracter('w', new ContextoCaracter(new Posicion(3, 0), formatoTitulo, true, "Ana"));
        documento.agregarCaracter('e', new ContextoCaracter(new Posicion(4, 0), formatoTitulo, true, "Ana"));
        documento.agregarCaracter('i', new ContextoCaracter(new Posicion(5, 0), formatoTitulo, true, "Ana"));
        documento.agregarCaracter('g', new ContextoCaracter(new Posicion(6, 0), formatoTitulo, true, "Ana"));
        documento.agregarCaracter('h', new ContextoCaracter(new Posicion(7, 0), formatoTitulo, true, "Ana"));
        documento.agregarCaracter('t', new ContextoCaracter(new Posicion(8, 0), formatoTitulo, true, "Ana"));

        documento.agregarCaracter('P', new ContextoCaracter(new Posicion(0, 2), formatoParrafo, false, "Luis"));
        documento.agregarCaracter('a', new ContextoCaracter(new Posicion(1, 2), formatoParrafo, false, "Luis"));
        documento.agregarCaracter('t', new ContextoCaracter(new Posicion(2, 2), formatoParrafo, false, "Luis"));
        documento.agregarCaracter('r', new ContextoCaracter(new Posicion(3, 2), formatoParrafo, false, "Luis"));
        documento.agregarCaracter('o', new ContextoCaracter(new Posicion(4, 2), formatoParrafo, false, "Luis"));
        documento.agregarCaracter('n', new ContextoCaracter(new Posicion(5, 2), formatoParrafo, false, "Luis"));

        System.out.printf("Flyweights creados: %d%n", fabrica.cantidadFlyweights());
    }
}

17.7 Explicación del flujo

El cliente solicita glifos a la fábrica. Si el carácter ya existe, la fábrica lo reutiliza para evitar crear una nueva instancia. Cada vez que se dibuja un glifo, el contexto extrínseco se provee mediante ContextoCaracter, que encapsula posición, formato, resaltado y autor. Gracias a la compartición, el texto completo puede renderizarse con un número reducido de flyweights aunque existan miles de caracteres en pantalla.

El método cantidadFlyweights permite auditar la reutilización lograda y es una herramienta útil para pruebas automatizadas o perfiles de memoria.

17.8 Uso del patrón en el ecosistema Java

Muchas clases de la plataforma aplican ideas de Flyweight. Por ejemplo, la documentación oficial de Integer.valueOf explica cómo se reutilizan objetos para ciertos rangos numéricos, reduciendo el costo de autoboxing. De forma similar, el método String.intern() permite compartir literales y simboliza un flyweight administrado por la JVM. Frameworks de motores gráficos y librerías de caché implementan patrones equivalentes cuando administran texturas, sprites o fragmentos de consultas.

17.9 Riesgos y malas prácticas

El peligro más frecuente es compartir estado mutable por accidente. Si el flyweight expone setters o contiene referencias modificables, todos los clientes podrían verse afectados al alterarlo. Otra mala práctica es delegar demasiado código en el cliente, provocando duplicaciones al preparar el estado extrínseco.

El patrón también puede dificultar la depuración porque el número real de instancias no coincide con las entidades lógicas. Además, la búsqueda en grandes cachés puede generar una sobrecarga si no se utilizan estructuras eficientes o claves correctamente normalizadas.

17.10 Buenas prácticas para aplicar Flyweight

  • Defina claramente qué atributos son intrínsecos y asegure su inmutabilidad.
  • Implemente una fábrica responsable de cachear y entregar flyweights, evitando que los clientes creen instancias directas.
  • Combine la compartición con estructuras de datos compactas para el estado extrínseco, como registros o structs ligeros.
  • Utilice perfiles de memoria para validar que el ahorro compense la complejidad introducida.

17.11 Relación con otros patrones

Flyweight suele trabajar junto a Factory Method o Abstract Factory, que concentran la creación de objetos compartidos. Puede complementarse con Composite para representar jerarquías livianas y con Prototype cuando se necesita clonar configuraciones base antes de convertirlas en flyweights. Además, puede integrarse con Proxy para administrar el ciclo de vida de los objetos compartidos y con Strategy cuando el comportamiento extrínseco varía entre clientes.