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.
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.
La intención del patrón es usar compartición para soportar grandes cantidades de objetos de forma eficiente. Se justifica cuando:
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.
La estructura clásica del patrón identifica los siguientes roles:
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.
La clave del patrón es diferenciar correctamente los tipos de estado:
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).
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.
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());
}
}
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.
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.
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.
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.