Padrão Estrutural (GoF)

Flyweight

Compartilha estado intrínseco imutável entre grandes quantidades de objetos semelhantes, externalizando o estado extrínseco dependente de contexto — reduzindo drasticamente o uso de memória quando há milhares de instâncias quase idênticas.

Intenção

Compartilhar objetos de granularidade fina para suportar grandes quantidades deles de forma eficiente. O Flyweight separa o estado do objeto em duas partes: o estado intrínseco (imutável, independente do contexto, compartilhado entre muitas instâncias) e o estado extrínseco (variável, dependente do contexto, fornecido pelo cliente no momento do uso).

Catalogado pelo GoF (1994) como padrão estrutural, o Flyweight é indicado exclusivamente em cenários onde a quantidade de objetos é tão grande que o custo de memória se torna proibitivo. Uma fábrica central (FlyweightFactory) funciona como cache: retorna a mesma instância para objetos com o mesmo estado intrínseco, em vez de criar um novo objeto a cada vez. O cliente passa o estado extrínseco como argumento no momento em que usa o Flyweight — nunca o armazenando dentro do objeto compartilhado.

Problema

Imagine um jogo de partículas onde explosões criam milhares de fragmentos simultaneamente. A abordagem ingênua cria um objeto completo para cada partícula:

// Abordagem ingênua — NÃO faça isso com 100 000 partículas:
class Particula {
  tipo: string;       // "fogo" | "fumaca" | "estilhaco"
  cor: string;        // "#FF4500"
  textura: string;    // dados do sprite — pode ter kilobytes
  x: number;
  y: number;
  velocidadeX: number;
  velocidadeY: number;
}

// 100 000 instâncias × (tipo + cor + textura) = memória esgotada
const particulas: Particula[] = [];
for (let i = 0; i < 100_000; i++) {
  particulas.push({
    tipo: 'fogo', cor: '#FF4500', textura: spriteFogo, // repetido 100 000×
    x: Math.random() * 800, y: Math.random() * 600,
    velocidadeX: Math.random(), velocidadeY: Math.random(),
  });
}

O problema é óbvio: tipo, cor e textura são idênticos para todas as partículas do mesmo tipo. Estamos replicando esses dados 100 000 vezes, enquanto apenas x, y, velocidadeX e velocidadeY variam por instância. O Flyweight resolve isso compartilhando o que é comum e recebendo o que varia como argumento.

A distinção central do padrão:

  • Estado intrínseco — imutável, independente do contexto: tipo de partícula, cor, dados de textura/sprite. É o que pode ser compartilhado com segurança porque nunca muda por instância.
  • Estado extrínseco — variável, dependente do contexto: posição x/y, velocidade, tempo de vida. É fornecido pelo cliente (ParticulaContext) a cada operação — nunca fica armazenado dentro do Flyweight.

Solução

O Flyweight organiza o código em três participantes:

  1. Flyweight: armazena exclusivamente o estado intrínseco. Seus métodos recebem o estado extrínseco como parâmetro — nunca o guardam como campo. Ex.: TipoParticula com tipo, cor e textura.
  2. FlyweightFactory: mantém um cache (Map) de instâncias Flyweight indexadas pela chave do estado intrínseco. Retorna uma instância existente se a chave já existe, ou cria e armazena uma nova. É o elemento central do padrão — sem ela, o compartilhamento não acontece. É frequentemente implementada como Singleton.
  3. Context (cliente): armazena o estado extrínseco e uma referência ao Flyweight compartilhado. Ex.: ParticulaContext com x, y, velocidadeX, velocidadeY e uma referência a TipoParticula. A quantidade de Contexts pode ser enorme; a quantidade de Flyweights é pequena (uma por valor único de estado intrínseco).

A economia de memória vem do número de instâncias Flyweight criadas. Se há 3 tipos de partícula e 100 000 partículas, existem apenas 3 objetos Flyweight — não 100 000.

Estrutura

       TipoParticula (Flyweight)
  ┌──────────────────────────────────────────┐
  │ + tipo: string       ← intrínseco        │
  │ + cor: string        ← intrínseco        │
  │ + textura: string    ← intrínseco        │
  │                                          │
  │ + renderizar(x, y, vx, vy): void         │
  │   ↑ estado extrínseco vem como argumento │
  └──────────────────────────────────────────┘
              ▲ criado e cacheado por
  ┌──────────────────────────────────────────┐
  │    TipoParticulaFactory (Factory)        │
  │                                          │
  │ - cache: Map<string, TipoParticula>      │
  │ + obter(tipo, cor, textura):             │
  │     TipoParticula                        │
  │   → retorna do cache ou cria novo        │
  │ + contarInstancias(): number             │
  └──────────────────────────────────────────┘
              ▲ referenciado por
  ┌──────────────────────────────────────────┐
  │    ParticulaContext (Context)            │
  │                                          │
  │ - x: number          ← extrínseco        │
  │ - y: number          ← extrínseco        │
  │ - velocidadeX: number ← extrínseco       │
  │ - velocidadeY: number ← extrínseco       │
  │ - tipo: TipoParticula ← flyweight ref    │
  │                                          │
  │ + mover(): void                          │
  │ + renderizar(): void                     │
  │   → this.tipo.renderizar(x, y, vx, vy)  │
  └──────────────────────────────────────────┘


Contagem de instâncias (exemplo com 100 000 partículas):

  Sem Flyweight:  100 000 objetos Particula completos
  Com Flyweight:       3 objetos TipoParticula (fogo, fumaça, estilhaço)
                + 100 000 objetos ParticulaContext (leve — só números)

Exemplos de código

Exemplo 1 — Partículas num jogo com FlyweightFactory e contagem de instâncias

O exemplo abaixo demonstra a separação entre estado intrínseco (TipoParticula) e extrínseco (ParticulaContext), a factory com cache e a contagem de instâncias criadas — evidenciando a economia de memória.

// ── Flyweight — armazena APENAS estado intrínseco ─────────────
class TipoParticula {
  constructor(
    readonly tipo: string,      // intrínseco: imutável
    readonly cor: string,       // intrínseco: imutável
    readonly textura: string    // intrínseco: sprite/dados pesados
  ) {}

  // Estado extrínseco (x, y, vx, vy) chega como argumento — nunca é campo.
  renderizar(x: number, y: number, vx: number, vy: number): void {
    console.log(
      `[${this.tipo}] cor=${this.cor} @ (${x.toFixed(1)},${y.toFixed(1)})` +
      ` vel=(${vx.toFixed(2)},${vy.toFixed(2)})`
    );
  }
}

// ── FlyweightFactory — cache de instâncias por chave intrínseca ─
class TipoParticulaFactory {
  private static readonly cache = new Map<string, TipoParticula>();

  static obter(tipo: string, cor: string, textura: string): TipoParticula {
    const chave = `${tipo}|${cor}`;
    if (!TipoParticulaFactory.cache.has(chave)) {
      console.log(`  [Factory] Criando novo Flyweight: "${chave}"`);
      TipoParticulaFactory.cache.set(chave, new TipoParticula(tipo, cor, textura));
    }
    return TipoParticulaFactory.cache.get(chave)!;
  }

  static contarInstancias(): number {
    return TipoParticulaFactory.cache.size;
  }
}

// ── Context — armazena estado extrínseco + referência ao Flyweight
class ParticulaContext {
  private tipo: TipoParticula;

  constructor(
    tipoNome: string,
    cor: string,
    textura: string,
    private x: number,
    private y: number,
    private velocidadeX: number,
    private velocidadeY: number
  ) {
    // Obtém (ou cria) o Flyweight compartilhado
    this.tipo = TipoParticulaFactory.obter(tipoNome, cor, textura);
  }

  mover(dt: number): void {
    this.x += this.velocidadeX * dt;
    this.y += this.velocidadeY * dt;
  }

  renderizar(): void {
    // Passa estado extrínseco como argumento — não está no Flyweight
    this.tipo.renderizar(this.x, this.y, this.velocidadeX, this.velocidadeY);
  }
}

// ── Simulação ─────────────────────────────────────────────────
const TOTAL = 10_000;
const TIPOS = [
  { tipo: 'fogo',      cor: '#FF4500', textura: 'sprite_fogo.png'     },
  { tipo: 'fumaca',    cor: '#808080', textura: 'sprite_fumaca.png'   },
  { tipo: 'estilhaco', cor: '#C0C0C0', textura: 'sprite_estilhaco.png'},
];

console.log(`Criando ${TOTAL} partículas de ${TIPOS.length} tipos:`);

const particulas: ParticulaContext[] = [];
for (let i = 0; i < TOTAL; i++) {
  const t = TIPOS[i % TIPOS.length];
  particulas.push(new ParticulaContext(
    t.tipo, t.cor, t.textura,
    Math.random() * 800,
    Math.random() * 600,
    (Math.random() - 0.5) * 5,
    (Math.random() - 0.5) * 5
  ));
}

// Resultado:
//   [Factory] Criando novo Flyweight: "fogo|#FF4500"
//   [Factory] Criando novo Flyweight: "fumaca|#808080"
//   [Factory] Criando novo Flyweight: "estilhaco|#C0C0C0"

console.log(`\nInstâncias TipoParticula (Flyweight): ${TipoParticulaFactory.contarInstancias()}`);
console.log(`Instâncias ParticulaContext (Context): ${TOTAL}`);
// Instâncias TipoParticula (Flyweight): 3
// Instâncias ParticulaContext (Context): 10000

// Renderiza as primeiras 3 para demonstrar
particulas.slice(0, 3).forEach(p => p.renderizar());
// [fogo]      cor=#FF4500 @ (327.4,198.6) vel=( 2.13,-1.07)
// [fumaca]    cor=#808080 @ (542.1,391.2) vel=(-0.87, 3.45)
// [estilhaco] cor=#C0C0C0 @ ( 12.9,544.8) vel=( 1.22,-2.98)

Exemplo 2 — Caracteres tipográficos num editor de texto

O exemplo clássico do GoF: um editor com 500 000 caracteres. O estado intrínseco é a forma do glifo (família, peso, tamanho da fonte — imutável para todos os "A" na mesma fonte). O estado extrínseco é a posição de cada caractere na página (linha, coluna — único por instância). Sem Flyweight: 500 000 objetos com dados de fonte repetidos. Com Flyweight: um objeto por glifo único, compartilhado por todas as ocorrências.

// ── Flyweight: dados da fonte (intrínseco) ─────────────────────
class Glifo {
  constructor(
    readonly caractere: string,
    readonly familia: string,   // ex.: "Helvetica"
    readonly peso: string,      // ex.: "regular" | "bold"
    readonly tamanho: number    // ex.: 12 (pt)
  ) {}

  // Posição (extrínseca) vem como argumento
  desenhar(linha: number, coluna: number): void {
    console.log(
      `'${this.caractere}' [${this.familia} ${this.peso} ${this.tamanho}pt]` +
      ` → linha ${linha}, col ${coluna}`
    );
  }
}

// ── FlyweightFactory ──────────────────────────────────────────
class GlifoFactory {
  private readonly cache = new Map<string, Glifo>();

  obter(
    caractere: string,
    familia: string,
    peso: string,
    tamanho: number
  ): Glifo {
    const chave = `${caractere}|${familia}|${peso}|${tamanho}`;
    if (!this.cache.has(chave)) {
      this.cache.set(chave, new Glifo(caractere, familia, peso, tamanho));
    }
    return this.cache.get(chave)!;
  }

  contarGlifos(): number { return this.cache.size; }
}

// ── Context: posição de cada caractere no documento ───────────
class CaractereNoDocumento {
  private readonly glifo: Glifo;

  constructor(
    caractere: string,
    familia: string,
    peso: string,
    tamanho: number,
    private readonly linha: number,
    private readonly coluna: number,
    factory: GlifoFactory
  ) {
    this.glifo = factory.obter(caractere, familia, peso, tamanho);
  }

  renderizar(): void {
    this.glifo.desenhar(this.linha, this.coluna);
  }
}

// ── Uso ──────────────────────────────────────────────────────
const factory = new GlifoFactory();
const texto   = 'Hello'; // cada letra ocorre em múltiplas posições

// Simula duas linhas com o mesmo texto — mesmos glifos, posições distintas
const documento: CaractereNoDocumento[] = [];
for (let linha = 0; linha < 2; linha++) {
  for (let col = 0; col < texto.length; col++) {
    documento.push(new CaractereNoDocumento(
      texto[col], 'Helvetica', 'regular', 12,
      linha, col, factory
    ));
  }
}

// "Hello" × 2 linhas = 10 instâncias de CaractereNoDocumento
// mas apenas 5 glifos únicos (H,e,l,o — 'l' reutilizado 4×)
console.log(`Glifos únicos (Flyweight): ${factory.contarGlifos()}`); // 4
console.log(`Contextos (instâncias):    ${documento.length}`);       // 10

// Renderiza linha 0
documento.filter((_, i) => i < texto.length).forEach(c => c.renderizar());
// 'H' [Helvetica regular 12pt] → linha 0, col 0
// 'e' [Helvetica regular 12pt] → linha 0, col 1
// 'l' [Helvetica regular 12pt] → linha 0, col 2
// 'l' [Helvetica regular 12pt] → linha 0, col 3  ← mesmo Glifo 'l'
// 'o' [Helvetica regular 12pt] → linha 0, col 4

Quando usar

  • Quando o número de objetos é muito grande e o custo de memória é mensurável: Flyweight só compensa depois de perfilar. O critério é objetivo: existe um gargalo real de memória causado por objetos quase idênticos em quantidade massiva (milhares ou mais).
  • Quando grande parte do estado de cada objeto é idêntica entre instâncias: se os objetos diferem apenas em alguns campos numéricos (posição, tempo de vida), mas compartilham dados pesados (sprites, esquemas de cor, regras de negócio), o estado intrínseco compartilhável é substancial.
  • Quando a identidade dos objetos não importa para o cliente: o cliente não pode distinguir um Flyweight reutilizado de um novo objeto — e não precisa. Se a identidade de instância é relevante (ex.: dois objetos "iguais" têm ciclos de vida independentes), Flyweight não é adequado.

Quando evitar

  • Antes de perfilar: Flyweight introduz complexidade real — estado extrínseco se torna responsabilidade do cliente, o código fica mais difícil de entender e depurar. Aplicar sem evidência de problema de memória é over-engineering clássico.
  • Quando o estado intrínseco é pequeno ou não repetitivo: se cada objeto tem dados únicos, não há o que compartilhar. O padrão não traz benefício algum.
  • Quando o número de objetos é razoável: centenas de objetos com dados duplicados não justificam a complexidade. A regra prática: se a memória ocupada não é um problema mensurável, não aplique.

Prós e contras

Prós

  • Redução drástica no uso de memória quando há muitos objetos com estado intrínseco compartilhável.
  • Pode melhorar a performance de cache de CPU ao reduzir o número de objetos distintos percorridos.
  • A FlyweightFactory centraliza a criação e o compartilhamento — o cliente não precisa gerenciar o cache manualmente.

Contras

  • O estado extrínseco passa a ser responsabilidade do cliente — quem chama o Flyweight deve fornecer e gerenciar esses dados, aumentando o acoplamento.
  • Aumenta a complexidade do código: o que era um único objeto vira um par Flyweight + Context, mais a factory.
  • Pode introduzir bugs sutis se estado mutável vazar para o Flyweight compartilhado (viola a premissa de imutabilidade intrínseca).
  • O benefício de memória pode ser anulado pelo overhead de gerenciar e passar o estado extrínseco em cada operação.

Armadilhas comuns

1. Estado intrínseco mutável

A premissa fundamental do Flyweight é que o estado intrínseco é imutável. Se um Flyweight compartilhado for mutado por um cliente, todos os outros Contexts que o referenciam serão afetados — um bug sutil e difícil de rastrear. A proteção em TypeScript é usar readonly em todos os campos e não expor setters.

Regra: se você sentir a necessidade de modificar um campo do Flyweight, provavelmente esse campo é extrínseco e pertence ao Context — não ao Flyweight.

2. Aplicar antes de perfilar (over-engineering)

O Flyweight é um padrão de otimização de memória — não de design. Aplicá-lo antes de identificar um gargalo real é custo sem benefício: a separação entre intrínseco e extrínseco torna o código significativamente mais complexo. Use um profiler para confirmar que o uso de memória por objetos repetidos é o problema real antes de refatorar.

3. Estado extrínseco que vira intrínseco acidentalmente

A distinção entre intrínseco e extrínseco nem sempre é óbvia. Um campo como cor pode ser intrínseco (todos os "A" em negrito são azuis) ou extrínseco (cada caractere pode ter cor independente). Classificar incorretamente significa que o Flyweight terá estado que deveria variar por instância — tornando o compartilhamento incorreto.

A pergunta diagnóstica: "se dois objetos têm este valor diferente, eles ainda podem compartilhar a mesma instância Flyweight?". Se não, o campo é extrínseco.

4. Objetos Flyweight usados por múltiplas threads

Porque os Flyweights são compartilhados e (por design) imutáveis, eles são naturalmente thread-safe para leitura. O risco é a própria FlyweightFactory: a criação e inserção no cache deve ser protegida em ambientes multithreaded (mutex, sincronização) para evitar que dois threads criem o mesmo Flyweight simultaneamente.

Padrões relacionados

O Flyweight interage com padrões que também lidam com compartilhamento, composição de objetos e criação centralizada:

O Composite frequentemente usa Flyweight: quando uma árvore Composite contém muitas folhas repetidas (ex.: caracteres num documento, tiles num mapa), as folhas podem ser implementadas como Flyweights para economizar memória — a árvore cresce, mas o número de objetos únicos não. O Singleton é o padrão mais próximo da FlyweightFactory: a factory é frequentemente implementada como Singleton para garantir que o cache de Flyweights seja único em toda a aplicação. O Visitor pode ser combinado com Flyweight para percorrer e operar sobre as instâncias sem precisar que cada uma tenha o comportamento de visitação embutido — o Visitor recebe o estado extrínseco do Context e o Flyweight compartilhado como parâmetros.