cv.cm

Programación · Patrones de diseño

Patrón Flyweight explicado con ejemplos

5 min de lectura

Respuesta corta

Flyweight es un patrón de diseño estructural que permite a un programa manejar un número muy grande de objetos similares compartiendo las partes que tienen en común. Cada objeto se divide en estado intrínseco (compartido, inmutable, almacenado una vez en un flyweight) y estado extrínseco (único en cada uso, pasado por quien llama). Una fábrica guarda en caché los flyweights para que el mismo estado intrínseco nunca se almacene dos veces.

Intención y el problema que resuelve

Intención: usar la compartición para manejar de forma eficiente grandes cantidades de objetos de grano fino. El patrón viene del libro de la Banda de los Cuatro (Gang of Four, GoF) y pertenece a la categoría estructural.

Imagina un mapa que dibuja un millón de árboles. Cada árbol tiene una posición (x, y) y un tipo: nombre de la especie, color y una imagen de textura de varios kilobytes. Si cada objeto árbol guarda su propia copia de la textura, el uso de memoria crece con el número de árboles, y el programa puede quedarse sin memoria. Sin embargo, solo hay un puñado de especies. La textura, el nombre y el color son iguales para todos los robles.

Flyweight traslada los datos repetidos a un pequeño número de objetos compartidos. Un árbol guarda entonces solo su posición más una referencia al tipo de árbol compartido. Un millón de árboles con cinco especies necesitan cinco copias de la textura en lugar de un millón.

Estado intrínseco y extrínseco

El paso clave es dividir los datos de cada objeto en dos tipos:

EstadoSignificadoEjemplo del árbol
IntrínsecoIndependiente del contexto, idéntico para muchos objetos, almacenado dentro del flyweight y nunca modificadoNombre de la especie, color, textura
ExtrínsecoDepende del contexto, distinto en cada uso, lo guarda el cliente y se pasa a los métodos del flyweightPosición x e y

Como un flyweight se comparte, debe ser inmutable. Si un cliente pudiera cambiar el color del tipo de roble compartido, todos los robles del mapa cambiarían.

Estructura y participantes

RolResponsabilidad
FlyweightGuarda el estado intrínseco y expone operaciones que reciben el estado extrínseco como parámetros
Fábrica de flyweightsCrea y guarda en caché los flyweights; devuelve uno existente cuando se vuelve a pedir el mismo estado intrínseco
ContextoAlmacena el estado extrínseco más una referencia a un flyweight (un contexto por cada objeto del mundo real)
ClientePide flyweights a la fábrica y conserva o calcula el estado extrínseco

Implementación en TypeScript

La fábrica usa un Map indexado por los valores intrínsecos. TreeType es el flyweight, Tree es el contexto y Forest es el cliente.

class TreeType {
  constructor(
    readonly name: string,
    readonly color: string,
    readonly texture: Uint8Array, // large, shared data
  ) {}
  draw(x: number, y: number): string {
    return this.name + " (" + this.color + ") at " + x + "," + y;
  }
}
class TreeTypeFactory {
  private static cache = new Map<string, TreeType>();
  static get(name: string, color: string): TreeType {
    const key = name + "|" + color;
    let type = this.cache.get(key);
    if (!type) {
      type = new TreeType(name, color, new Uint8Array(4096));
      this.cache.set(key, type);
    }
    return type;
  }
}
class Tree {
  constructor(readonly x: number, readonly y: number, readonly type: TreeType) {}
  draw(): string {
    return this.type.draw(this.x, this.y);
  }
}
const forest: Tree[] = [];
for (let i = 0; i < 100_000; i++) {
  const [name, color] = i % 2 === 0 ? ["Oak", "green"] : ["Birch", "white"];
  forest.push(new Tree(i, i * 2, TreeTypeFactory.get(name, color)));
}
const types = new Set(forest.map((t) => t.type));
console.log(forest.length, "trees,", types.size, "tree types");
console.log(forest[3].draw());
// 100000 trees, 2 tree types
// Birch (white) at 3,6

Sin la fábrica, el programa reservaría 100.000 búferes de textura. Con ella, hay dos. Los objetos Tree siguen existiendo, pero cada uno guarda solo dos números y una referencia.

Cuándo usarlo y cuándo no

Usa Flyweight cuando se cumplan todas estas condiciones:

  • El programa crea un número muy grande de objetos similares.
  • El uso de memoria es un problema medido, no una suposición.
  • La mayor parte del estado de cada objeto puede hacerse extrínseco o compartido.
  • El programa no depende de la identidad de los objetos (dos contextos con el mismo flyweight pueden compartirlo).

Evítalo cuando el número de objetos sea pequeño, cuando los objetos necesiten cambiar sus datos compartidos de forma independiente, o cuando la complejidad añadida de pasar el estado extrínseco de un lado a otro haga el código más difícil de leer que la memoria que ahorra. En muchos casos funciona una solución más simple: guardar los datos en arreglos paralelos o arreglos tipados, o internar las cadenas repetidas en un Map.

Contrapartida: Flyweight cambia memoria por tiempo de CPU. Puede que el estado extrínseco tenga que recalcularse o buscarse cada vez que se llama a un método.

Usos en el mundo real

  • Java: Integer.valueOf(int) devuelve objetos Integer en caché para valores de -128 a 127, así que empaquetar números pequeños no reserva objetos nuevos cada vez.
  • Java: String.intern() devuelve una copia canónica compartida de una cadena del conjunto de cadenas.
  • Los editores de texto y los navegadores dibujan glifos desde una caché de fuentes compartida: cada carácter en pantalla guarda su posición, mientras que la forma del glifo se almacena una vez por fuente y tamaño.
  • Los motores de juegos usan renderizado por instancias: se comparten una malla y un material, y solo se envían para cada copia los datos por instancia, como la posición y la rotación.

Patrones relacionados

PatrónEn qué se diferencia de Flyweight
SingletonSingleton garantiza una instancia de una clase; Flyweight mantiene una instancia por cada estado intrínseco distinto, así que puede haber muchas.
Factory MethodUna fábrica de flyweights es una fábrica con caché; Factory Method trata de dejar que las subclases elijan qué clase crear.
CompositeLos nodos hoja de un árbol Composite suelen implementarse como flyweights para que las hojas repetidas compartan memoria.
PrototypePrototype copia un objeto para crear uno nuevo; Flyweight evita copiar compartiendo un solo objeto.

Preguntas frecuentes

¿Cuál es la diferencia entre estado intrínseco y extrínseco?

El estado intrínseco es compartido e idéntico para muchos objetos, así que vive dentro del flyweight y nunca cambia. El estado extrínseco es distinto en cada uso, así que el cliente lo guarda y lo pasa a los métodos del flyweight.

¿Flyweight es lo mismo que el almacenamiento en caché?

Usa una caché, pero el objetivo es distinto. Una caché evita recalcular un resultado, mientras que Flyweight evita almacenar muchas veces los mismos datos inmutables compartiendo un objeto entre muchos usuarios.

¿Por qué un flyweight debe ser inmutable?

Muchos contextos tienen una referencia al mismo flyweight. Si uno de ellos lo cambiara, el cambio aparecería en todos los demás contextos que lo comparten.

¿Flyweight es útil en JavaScript y TypeScript?

Sí, cuando creas cientos de miles de objetos, por ejemplo partículas, mosaicos de mapa o celdas de cuadrícula. Los objetos de JavaScript tienen una sobrecarga por objeto, así que compartir los datos repetidos puede reducir la memoria y el trabajo del recolector de basura.

Todos los tutoriales de programación