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:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
public interface IGlifo
{
char Caracter { get; }
void Dibujar(ContextoCaracter contexto);
}
public class Glifo : IGlifo
{
public Glifo(char caracter)
{
Caracter = caracter;
}
public char Caracter { get; }
public void Dibujar(ContextoCaracter contexto)
{
Console.WriteLine($"Caracter: {Caracter} | Posicion: {contexto.Posicion.X},{contexto.Posicion.Y} | Fuente: {contexto.FormatoFuente.Nombre} {contexto.FormatoFuente.Tamano} | Autor: {contexto.Autor}");
}
}
public class GlifoFactory
{
private readonly ConcurrentDictionary<char, IGlifo> _flyweights = new ConcurrentDictionary<char, IGlifo>();
public IGlifo Obtener(char caracter)
{
return _flyweights.GetOrAdd(caracter, c => new Glifo(c));
}
public int CantidadFlyweights() => _flyweights.Count;
}
public record Posicion(int X, int Y);
public record FormatoFuente(string Nombre, int Tamano, bool Negrita, bool Cursiva);
public record ContextoCaracter(Posicion Posicion, FormatoFuente FormatoFuente, bool Resaltado, string Autor);
public class Documento
{
private readonly GlifoFactory _fabrica;
private readonly List<(IGlifo Glifo, ContextoCaracter Contexto)> _contenido = new List<(IGlifo, ContextoCaracter)>();
public Documento(GlifoFactory fabrica)
{
_fabrica = fabrica;
}
public void AgregarCaracter(char caracter, ContextoCaracter contexto)
{
var glifo = _fabrica.Obtener(caracter);
_contenido.Add((glifo, contexto));
}
public void Dibujar()
{
foreach (var (glifo, contexto) in _contenido)
{
glifo.Dibujar(contexto);
}
}
}
public static class DemoFlyweight
{
public static void Main()
{
var fabrica = new GlifoFactory();
var documento = new Documento(fabrica);
var formatoTitulo = new FormatoFuente("Roboto", 18, true, false);
var formatoParrafo = new FormatoFuente("Fira Code", 12, false, false);
documento.AgregarCaracter('H', new ContextoCaracter(new Posicion(0, 0), formatoTitulo, true, "Ana"));
documento.AgregarCaracter('e', new ContextoCaracter(new Posicion(1, 0), formatoTitulo, true, "Ana"));
documento.AgregarCaracter('l', new ContextoCaracter(new Posicion(2, 0), formatoTitulo, true, "Ana"));
documento.AgregarCaracter('l', new ContextoCaracter(new Posicion(3, 0), formatoTitulo, true, "Ana"));
documento.AgregarCaracter('o', new ContextoCaracter(new Posicion(4, 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"));
documento.Dibujar();
Console.WriteLine($"Flyweights creados: {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.
Diversas clases de .NET aplican ideas de Flyweight. Por ejemplo, el método string.Intern
comparte literales en un pool administrado por el runtime, mientras que System.Text.Rune
representa puntos de código Unicode de forma ligera. En motores gráficos como MonoGame o Unity se emplean flyweights para gestionar sprites, texturas y materiales, evitando cargar los mismos recursos una y otra vez.
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.