StackPractices
intermediate Por Mathias Paulenko

Flyweight Pattern para Comparticion Eficiente de Objetos

Usa el Flyweight pattern para minimizar uso de memoria compartiendo la mayor cantidad de datos posible entre objetos similares, esencial para renderizar datasets grandes

El Flyweight pattern minimiza el uso de memoria compartiendo la mayor cantidad de datos posible entre objetos similares. Cuando una aplicacion necesita crear miles de objetos que comparten la mayor parte de su estado, Flyweight extrae el estado compartido (intrinseco) en un objeto compartido separado, dejando solo el estado unico (extrinseco) en cada instancia.

Cuando Usar Esto

  • Una aplicacion usa una gran cantidad de objetos con estado compartido. Consulta Singleton Pattern para gestionar instancias únicas.
  • El costo de memoria es alto por la cantidad de objetos. Consulta Caching Strategies para reducir almacenamiento redundante.
  • La mayor parte del estado del objeto puede hacerse extrinseco y computarse on the fly. Consulta Object Pool para patrones de instancias reutilizables.

Problema

Un editor de documentos con 100,000 caracteres crea 100,000 objetos Character. Cada uno almacena fuente, tamano, color y datos de glifo — incluso cuando solo existen 200 estilos de caracter unicos en el documento.

Solucion

// flyweight/CharacterStyle.ts
interface CharacterStyle {
  font: string;
  size: number;
  color: string;
  bold: boolean;
}

class StyleFactory {
  private styles = new Map<string, CharacterStyle>();

  getStyle(font: string, size: number, color: string, bold: boolean): CharacterStyle {
    const key = `${font}-${size}-${color}-${bold}`;

    if (!this.styles.has(key)) {
      this.styles.set(key, { font, size, color, bold });
    }

    return this.styles.get(key)!;
  }

  getStyleCount(): number {
    return this.styles.size;
  }
}

// Flyweight character con posicion extrinseca
class Character {
  constructor(
    private char: string,
    private style: CharacterStyle  // Shared intrinsic state
  ) {}

  render(position: number): string {
    // Estado extrinseco: posicion pasada en tiempo de renderizado
    return `<span style="font: ${this.style.size}px ${this.style.font}; color: ${this.style.color}; ${this.style.bold ? 'font-weight: bold;' : ''}" data-position="${position}">${this.char}</span>`;
  }
}

// Documento usa flyweights
class Document {
  private characters: { char: Character; position: number }[] = [];
  private styleFactory = new StyleFactory();

  insert(char: string, position: number, font: string, size: number, color: string, bold: boolean): void {
    const style = this.styleFactory.getStyle(font, size, color, bold);
    const character = new Character(char, style);
    this.characters.push({ char: character, position });
  }

  render(): string {
    return this.characters
      .map(c => c.char.render(c.position))
      .join('');
  }

  getMemoryStats(): { characters: number; uniqueStyles: number } {
    return {
      characters: this.characters.length,
      uniqueStyles: this.styleFactory.getStyleCount(),
    };
  }
}

// Uso
const doc = new Document();

// Insertar 10,000 caracteres usando solo 3 estilos unicos
doc.insert('H', 0, 'Arial', 12, '#000', true);
doc.insert('e', 1, 'Arial', 12, '#000', true);

for (let i = 2; i < 10000; i++) {
  doc.insert('x', i, 'Arial', 12, '#000', false);
}

console.log(doc.getMemoryStats());
// { characters: 10000, uniqueStyles: 2 }

Variacion: Pool de Objetos de Juego

// flyweight/Tree.ts
interface TreeType {
  mesh: string;
  barkTexture: string;
  leafTexture: string;
}

class TreeTypeFactory {
  private types = new Map<string, TreeType>();

  getTreeType(mesh: string, bark: string, leaf: string): TreeType {
    const key = `${mesh}-${bark}-${leaf}`;
    if (!this.types.has(key)) {
      this.types.set(key, { mesh, barkTexture: bark, leafTexture: leaf });
    }
    return this.types.get(key)!;
  }
}

// Instancia de Tree solo almacena posicion y referencia de tipo
class Tree {
  constructor(
    private x: number,
    private y: number,
    private type: TreeType  // Shared flyweight
  ) {}

  render(): void {
    console.log(`Render ${this.type.mesh} at (${this.x}, ${this.y})`);
  }
}

// Bosque con miles de arboles usando pocos tipos
class Forest {
  private trees: Tree[] = [];
  private typeFactory = new TreeTypeFactory();

  plantTree(x: number, y: number, mesh: string, bark: string, leaf: string): void {
    const type = this.typeFactory.getTreeType(mesh, bark, leaf);
    this.trees.push(new Tree(x, y, type));
  }
}

Como Funciona

  1. Flyweight almacena el estado intrinseco (compartido) que pertenece a muchos objetos
  2. Context almacena el estado extrinseco (unico) y referencia un Flyweight
  3. Flyweight Factory crea y maneja instancias de flyweight compartidas
  4. Client computa estado extrinseco y lo pasa a los metodos del flyweight

Consideraciones de Produccion

  • Los flyweights deben ser inmutables; nunca modifiques estado compartido despues de la creacion
  • La seguridad de threads es requerida cuando la factory se accede concurrentemente
  • Considera usar WeakMap para garbage collection automatico de flyweights no usados

Errores Comunes

  • Poner estado extrinseco dentro de la clase Flyweight, derrotando el proposito
  • No usar una factory, permitiendo instancias duplicadas de flyweight
  • Modificar estado de flyweight compartido, corrompiendo todos los contexts que lo usan

Troubleshooting

  • Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
  • Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
  • Tight coupling after refactoring: check that interfaces are stable and dependencies point inward.
  • Tests break when the design changes: favor stable contracts over internal structure.
  • Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.

Temas Avanzados

Escenario: Flyweight para Renderizado de Texto

// Flyweight: compartir caracteres para reducir memoria
interface CharacterFlyweight {
  char: string;
  font: string;
  size: number;
  render(x: number, y: number): string;
}

class Character implements CharacterFlyweight {
  constructor(
    public char: string,
    public font: string,
    public size: number
  ) {}

  render(x: number, y: number): string {
    return `<text x="${x}" y="${y}" font-family="${this.font}" font-size="${this.size}">${this.char}</text>`;
  }
}

// Flyweight Factory: cachea caracteres compartidos
class CharacterFactory {
  private cache = new Map<string, CharacterFlyweight>();

  getCharacter(char: string, font: string, size: number): CharacterFlyweight {
    const key = `${char}|${font}|${size}`;
    if (!this.cache.has(key)) {
      this.cache.set(key, new Character(char, font, size));
      console.log(`[FLYWEIGHT] Created: ${key}`);
    }
    return this.cache.get(key)!;
  }
  getCacheSize(): number { return this.cache.size; }
}

// Contexto extrinseco: posicion (no se comparte)
class TextRenderer {
  constructor(private factory: CharacterFactory) {}

  renderText(text: string, font: string, size: number, startX: number, y: number): string {
    let x = startX;
    let output = "";
    for (const char of text) {
      const flyweight = this.factory.getCharacter(char, font, size);
      output += flyweight.render(x, y) + "\n";
      x += size * 0.6; // ancho aproximado
    }
    return output;
  }
}

// Uso: renderizar 10000 caracteres
const factory = new CharacterFactory();
const renderer = new TextRenderer(factory);

// Sin flyweight: 10000 objetos Character
// Con flyweight: ~100 objetos (unicos char+font+size)
const text = "Hola mundo ".repeat(1000);
const svg = renderer.renderText(text, "Arial", 12, 10, 50);
console.log(`Cache size: ${factory.getCacheSize()}`); // ~30 unicos

// Memoria estimada
  | Escenario | Objetos | Memoria |
  |-----------|---------|---------|
  | Sin flyweight | 12000 | 480KB |
  | Con flyweight | 30 | 1.2KB |
  | Ahorro | 99.75% | |

Lecciones:

  • Flyweight comparte estado intrinseco (char, font, size)
  • El estado extrinseco (x, y) no se comparte
  • La factory cachea los flyweights unicos
  • Ideal para grandes cantidades de objetos similares
  • Usar en editores de texto, juegos (tiles), renderizado SVG

### Cuando NO usar flyweight?

No uses flyweight cuando hay pocos objetos (el overhead de la factory supera el ahorro), cuando los objetos son mutables (flyweight requiere objetos inmutables), o cuando cada objeto tiene estado unico sin repeticion. El overhead de la factory y el Map de cache solo se compensa cuando hay alta repeticion del mismo estado intrinseco.


End of document. Review and update quarterly.




## Lectura Adicional

- **Documentación oficial**: consulta la referencia actualizada del framework o herramienta utilizada.
- **Guías relacionadas**: explora las guías de flyweight y structural-patterns para profundizar.
- **Patrones complementarios**: revisa los patrones de diseño aplicables a tu stack tecnológico.
- **Postmortems públicos**: estudia incidentes reales de equipos que enfrentaron problemas similares en producción.

## Notas de Producción

- **Despliega gradualmente** usando canary o blue-green para detectar regresiones temprano.
- **Configura alertas** para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
- **Documenta el rollback** en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
- **Revisa logs estructurados** con correlation IDs para trazar requests end-to-end en incidentes.

## Puntos Clave

- **Aplica flyweight pattern para comparticion eficiente de objetos** cuando necesites una solución práctica para tu caso de uso.
- **Monitorea el rendimiento** después de implementar; mide latencia, errores y uso de recursos antes y después.
- **Revisa la sección de Troubleshooting** ante errores comunes; la mayoría tienen causa raíz documentada con solución.
- **Mantén dependencias actualizadas** y ejecuta tests en CI para prevenir regresiones en producción.

## Errores Comunes en Producción

- Aplicar el patrón donde no se necesita abstracción, agregando complejidad accidental.
- Dejar que el patrón se filtre en módulos no relacionados y confundir los límites de responsabilidad.
- Sobre-ingeniería en la primera implementación en lugar de comenzar simple y medir el dolor.
- Saltar los tests de contrato, de modo que las refactorizaciones rompan consumidores en silencio.
- Ignorar modos de fallo que el patrón no cubre.
- Usar el patrón como opción por defecto en lugar de elegir la herramienta adecuada para la escala actual.
- Olvidar documentar cuándo dejar de usar el patrón y qué lo reemplaza.
- Carecer de observabilidad sobre rendimiento y propagación de errores del patrón.

Preguntas frecuentes

¿Es este patrón adecuado para proyectos pequeños?

Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.

¿Cómo se compara este patrón con alternativas?

Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.

¿Puedo aplicar este patrón parcialmente?

Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.