💾 El Patrón Flyweight: El gran ahorrador de memoria
¿Te ha pasado alguna vez que tu aplicación necesita crear miles o millones de objetos que son casi iguales y la RAM se va a la puta? Pues para eso existe el patrón de diseño Flyweight, un patrón estructural que va perfecto para ahorrar memoria compartiendo todo lo que se pueda entre objetos.
La idea es bastante de simple: si tienes 10.000 objetos y todos comparten los mismos datos pesados, ¿para qué copiar esos datos 10.000 veces? Creas una única instancia compartida y que todos la referencien. EA.
No dupliques lo que es igual. Guárdalo una vez y que todo el mundo tire de la misma copia.

💾 1. Estado intrínseco vs extrínseco
Esto es lo más importante del patrón, así que pay attention please:
Lo que es igual para todos los objetos del mismo tipo. No cambia. Se mete dentro del Flyweight y se comparte. Ejemplo: la textura de un árbol, el sprite de un enemigo, la fuente de un carácter.
Lo que es diferente en cada instancia. Cambia. Se guarda fuera del Flyweight y se le pasa cuando toca. Ejemplo: la posición X,Y de un árbol, la vida de un enemigo, el color de un carácter.
Todo el patrón se basa en esta separación. Lo que se repite va dentro del Flyweight (una sola copia para todos). Lo que varía se queda fuera y se pasa como parámetro cuando hace falta.
¿Qué es el estado intrínseco en un Flyweight?
🚗 2. La fábrica de coches
Imagina una fábrica que saca miles de coches:
-
Sin Flyweight: Cada coche lleva incrustada la imagen del logo de la marca (5MB). Si fabricas 1000 coches, son 5000 MB (5GB) solo en logos. Tu servidor llora.
-
Con Flyweight: La imagen del logo se guarda una sola vez. Los 1000 coches apuntan a esa única imagen. Cada coche solo guarda lo suyo: matrícula, GPS, color. Resultado: 5MB del logo + los datos de cada coche (que pesan nada). Tu servidor ni se entera.
¿Cuál es el principal objetivo del Patrón Flyweight?
🛠️ 3. Los roles del patrón
Guarda el estado intrínseco (lo común e inmutable). Cuando tiene que hacer algo, recibe el estado extrínseco como parámetro.
El cerebro del chiringuito. Si ya existe un Flyweight con esa clave, te lo devuelve. Si no, lo crea y lo guarda. Básicamente es una caché de objetos.
Guarda el estado extrínseco (lo que es único de cada uso). Cuando necesita operar, le pasa ese estado al Flyweight.
El cliente nunca crea Flyweights directamente. Siempre pasa por la Factory. Si te saltas la Factory y haces new, te cargas todo el beneficio del patrón.
¿Cuál es el rol de la Factoría del Flyweight?
🧑💻 4. Ejemplo práctico en PHP: Árboles en un juego
Imagina un RPG con miles de árboles. Todos los Robles tienen la misma textura (20MB) y el mismo modelo 3D. Lo que cambia es dónde está plantado cada uno.
<?php
// 1. Interfaz del Flyweight
interface TipoDeArbol {
public function dibujar(int $x, int $y): void;
}
// 2. Flyweight Concreto (guarda el estado intrínseco)
class ArbolFlyweight implements TipoDeArbol {
// Estado intrínseco: compartido e inmutable
private string $nombre;
private string $textura; // Simula un archivo pesado (20MB)
public function __construct(string $nombre, string $textura) {
$this->nombre = $nombre;
$this->textura = $textura;
echo " [FLYWEIGHT] Creado: $this->nombre (Textura: $this->textura)\n";
}
// Recibe el estado extrínseco (posición) como parámetro
public function dibujar(int $x, int $y): void {
echo " [DIBUJO] $this->nombre en ($x, $y) con textura $this->textura\n";
}
}
// 3. Factoría del Flyweight (caché de objetos)
class FactoriaArbol {
/** @var TipoDeArbol[] */
private array $flyweights = [];
public function getFlyweight(string $nombre, string $textura): TipoDeArbol {
if (!isset($this->flyweights[$nombre])) {
// No existe, lo creamos
$this->flyweights[$nombre] = new ArbolFlyweight($nombre, $textura);
}
// Ya existe, lo devolvemos (¡ahorro!)
return $this->flyweights[$nombre];
}
public function getNumFlyweights(): int {
return count($this->flyweights);
}
}
// 4. Uso del patrón (El Motor del Juego)
$factoria = new FactoriaArbol();
echo "--- 3 Robles (mismo tipo, distinta posición) ---\n";
$factoria->getFlyweight('Roble', 'roble_hd.jpg')->dibujar(10, 50);
$factoria->getFlyweight('Roble', 'roble_hd.jpg')->dibujar(20, 60);
$factoria->getFlyweight('Roble', 'roble_hd.jpg')->dibujar(30, 70);
echo "\n--- 2 Abetos (nuevo tipo) ---\n";
$factoria->getFlyweight('Abeto', 'abeto_hd.png')->dibujar(5, 10);
$factoria->getFlyweight('Abeto', 'abeto_hd.png')->dibujar(8, 12);
echo "\n--- Resumen ---\n";
echo "Árboles solicitados: 5\n";
echo "Flyweights en memoria: " . $factoria->getNumFlyweights() . "\n";
// Solo 2 objetos en memoria, no 5. ¡Ahorro del 60%!
// 🖥️ Salida:
// --- 3 Robles (mismo tipo, distinta posición) ---
// [FLYWEIGHT] Creado: Roble (Textura: roble_hd.jpg)
// [DIBUJO] Roble en (10, 50) con textura roble_hd.jpg
// [DIBUJO] Roble en (20, 60) con textura roble_hd.jpg
// [DIBUJO] Roble en (30, 70) con textura roble_hd.jpg
//
// --- 2 Abetos (nuevo tipo) ---
// [FLYWEIGHT] Creado: Abeto (Textura: abeto_hd.png)
// [DIBUJO] Abeto en (5, 10) con textura abeto_hd.png
// [DIBUJO] Abeto en (8, 12) con textura abeto_hd.png
//
// --- Resumen ---
// Árboles solicitados: 5
// Flyweights en memoria: 2
“[FLYWEIGHT] Creado” solo sale dos veces: una para Roble y otra para Abeto. Las demás llamadas reutilizan el que ya existe. Si tuvieras 10.000 robles, seguirías teniendo un solo Flyweight en memoria. Eso es lo bonito del asunto.
¿Por qué es un error guardar la posición (X, Y) dentro del Flyweight?
🌍 5. Ejemplo real en PHP: Caché de iconos
Los jueguecitos están bien para explicar, pero en backend PHP esto también se usa. Imagina un sistema que genera PDFs con iconos:
class IconoFlyweight {
private string $nombre;
private string $svgData; // El SVG puede pesar lo suyo
public function __construct(string $nombre, string $svgData) {
$this->nombre = $nombre;
$this->svgData = $svgData;
}
public function renderizar(int $tamaño, string $color): string {
// Estado intrínseco: svgData (compartido)
// Estado extrínseco: tamaño y color (único por uso)
return "<svg width='$tamaño' fill='$color'>{$this->svgData}</svg>";
}
}
class FactoriaIconos {
private array $iconos = [];
public function getIcono(string $nombre): IconoFlyweight {
if (!isset($this->iconos[$nombre])) {
// Simula cargar el SVG del disco
$svg = file_get_contents("/icons/$nombre.svg");
$this->iconos[$nombre] = new IconoFlyweight($nombre, $svg);
}
return $this->iconos[$nombre];
}
}
// Uso: un PDF con 500 líneas, cada una con el icono "check"
$factoria = new FactoriaIconos();
foreach ($lineasDelPdf as $linea) {
$icono = $factoria->getIcono('check'); // Siempre el mismo objeto
echo $icono->renderizar(16, $linea['color']); // Distinto color
}
// El SVG se cargó del disco UNA vez, no 500
Otro caso muy típico son los sistemas que renderizan fuentes tipográficas. Cada carácter tiene un glyph (la forma visual del carácter), que es el estado intrínseco. La posición, tamaño y color son el estado extrínseco. Un documento con 100.000 caracteres no necesita 100.000 glyphs en memoria, solo los del alfabeto (~52). Lo demás son posiciones.
¿En qué caso real de PHP backend podrías aplicar Flyweight?
📊 6. Números reales: ¿Cuánto ahorra?
Para que veas que no exagero, aquí tienes los números del ejemplo de los árboles:
| Escenario | Sin Flyweight | Con Flyweight | Ahorro |
|---|---|---|---|
| 100 árboles (2 tipos) | 100 × 20MB = 2.000 MB | 2 × 20MB + 100 × 24B = ~40 MB | 98% |
| 10.000 árboles (5 tipos) | 10.000 × 20MB = 200 GB | 5 × 20MB + 10.000 × 24B = ~100 MB | 99.95% |
| 1.000.000 caracteres (52 glyphs) | 1M × 50KB = 50 GB | 52 × 50KB + 1M × 8B = ~10 MB | 99.98% |
Cuantos más objetos tengas y más pese el estado intrínseco, más bestial es el ahorro. Pero ojo que si tus objetos pesan poco y son pocos, Flyweight es matar moscas a cañonazos.
🔄 7. Comparativa con otros patrones
| Patrón | Propósito | Diferencia clave |
|---|---|---|
| Flyweight | Compartir estado entre muchos objetos para ahorrar memoria | Múltiples objetos comparten una instancia inmutable |
| Singleton | Garantizar una sola instancia global | Singleton = una instancia global; Flyweight = varias instancias compartidas (una por tipo) |
| Prototype | Clonar objetos existentes | Prototype clona (más objetos); Flyweight comparte (menos objetos) |
| Proxy | Controlar el acceso a un objeto | Proxy controla acceso; Flyweight controla memoria |
| Object Pool | Reutilizar objetos costosos de crear | Pool reutiliza objetos mutables; Flyweight comparte objetos inmutables |
La confusión que más veo es Flyweight vs Singleton. La diferencia es que un Singleton es siempre una sola instancia global. Flyweight puede tener varias (una por tipo: un Flyweight para “Roble”, otro para “Abeto”, etc.). La Factory decide cuál te toca.
¿Cuál es la diferencia principal entre Flyweight y Singleton?
⚖️ 8. Relación con SOLID
Cada pieza a lo suyo: el Flyweight guarda el estado compartido, la Factoría gestiona instancias y el Cliente guarda el estado único. Nadie se mete en el trabajo del otro.
¿Quieres un tipo nuevo de Flyweight (ej. "Palmera")? Lo creas y la Factoría lo gestiona solita. Sin tocar nada de lo que ya tienes.
El cliente depende de la interfaz TipoDeArbol, no de ArbolFlyweight directamente. Puedes cambiar la implementación y no se rompe nada.
Cualquier implementación de TipoDeArbol encaja donde se espera la interfaz. El Flyweight concreto se puede sustituir sin dramas.
✅ 9. Ventajas y desventajas
- Ahorro brutal de memoria: Miles de objetos, una sola instancia compartida del estado pesado.
- Rendimiento: Menos objetos = menos trabajo para el recolector de basura, menos inicialización.
- Transparente: El cliente ni se entera de que está compartiendo objetos. Para él es como si fueran únicos.
- Escala de miedo: Funciona igual de bien con 100 que con 1.000.000 de objetos.
- Complejidad añadida: Separar estado intrínseco y extrínseco no es lo más natural del mundo.
- Incomodidad: El cliente tiene que andar pasando el estado extrínseco en cada llamada.
- Inmutabilidad obligatoria: El Flyweight no puede tener setters ni cambiar por dentro. Si lo haces, la lías.
- Overkill si hay pocos objetos: La Factoría y toda la infraestructura no compensan si solo tienes 10 cositas.
⚠️ 10. Errores comunes
1. Colar estado extrínseco dentro del Flyweight
Si el Flyweight guarda datos que son diferentes para cada uso, ya no se puede compartir y te cargas el patrón entero:
// ❌ MAL: la posición es única, no pinta nada en el Flyweight
class ArbolFlyweight {
private string $textura;
private int $x; // ← Esto es estado extrínseco
private int $y; // ← Esto también
public function dibujar(): void {
// Usa $this->x y $this->y directamente
}
}
// ✅ BIEN: la posición se pasa como parámetro
class ArbolFlyweight {
private string $textura; // Solo estado intrínseco
public function dibujar(int $x, int $y): void {
// Recibe x,y como parámetros (estado extrínseco)
}
}
2. Saltarse la Factoría
Si el cliente crea Flyweights a pelo con new, no hay compartición ninguna. Como si no existiera el patrón:
// ❌ MAL: crea objetos nuevos cada vez
$roble1 = new ArbolFlyweight('Roble', 'roble.jpg');
$roble2 = new ArbolFlyweight('Roble', 'roble.jpg'); // ¡Duplicado!
// ✅ BIEN: siempre a través de la Factoría
$roble1 = $factoria->getFlyweight('Roble', 'roble.jpg');
$roble2 = $factoria->getFlyweight('Roble', 'roble.jpg'); // Misma instancia
3. Flyweight con setters (mutable)
Si dejas que alguien cambie el estado interno del Flyweight, se lía parda. Recuerda que ese objeto lo comparten TODOS:
// ❌ MAL: el Flyweight tiene setters
class ArbolFlyweight {
public function setTextura(string $textura): void {
$this->textura = $textura; // ¡Cambia para TODOS los que lo comparten!
}
}
// Si cambias la textura del "Roble", TODOS los robles del juego cambian.
// Probablemente no es lo que quieres, nene.
El Flyweight tiene que ser inmutable. Si necesitas cambiar algo, crea un nuevo tipo.
¿Qué pasa si el Flyweight permite modificar su estado interno (tiene setters)?
4. Usarlo cuando no hace falta
Si solo tienes 20 objetos ligeros, montar la Factory y separar el estado es trabajar de más para nada. Flyweight es un patrón de optimización, no lo metas por si acaso:
// ❌ Sobreingeniería: 5 botones con un icono de 2KB
$factoria->getFlyweight('boton-guardar', 'save.svg');
// ✅ Para 5 botones, crea los objetos normales y déjate de historias
$boton = new Boton('Guardar', 'save.svg');
🤔 11. ¿Cuándo usarlo y cuándo no?
- Tienes miles o millones de objetos que se parecen mucho entre sí
- Los objetos tienen datos pesados que se repiten (texturas, fuentes, configuraciones)
- Puedes separar claramente el estado intrínseco del extrínseco
- El estado intrínseco es inmutable (no cambia una vez creado)
- Has medido que la memoria es un problema real, no una suposición
- Tienes pocos objetos (la movida no compensa)
- Los objetos son todos diferentes (no hay nada que compartir)
- El estado compartido es mutable (necesita cambiar según la instancia)
- La memoria no es un problema real — no optimices lo que no has medido
Si has perfilado tu aplicación y ves que cientos de objetos comparten los mismos datos pesados, tira de Flyweight. Si no has medido que la memoria es un problema, probablemente no lo necesitas. No seas ése que optimiza prematuramente.
¿Cuándo NO deberías usar Flyweight?
💡 12. Conclusión
El Flyweight es un patrón de optimización pura y dura. No te cambia la arquitectura ni te añade funcionalidad nueva. Lo que hace es que tu aplicación funcione donde antes se quedaba tiesa por falta de memoria.
La idea consiste en guardar solamente una vez lo que se repite. Lo que varía, pásalo como parámetro. Es lo que hacen los motores de videojuegos con las texturas, los procesadores de texto con las fuentes y los navegadores con los estilos CSS.
No es de los patrones que usas todos los días. Pero el día que lo necesitas, te marca la diferencia entre una aplicación que tira y una que se cae a la puta.
EA, ¡saluditos y nos vemos en los bares! 🍻