Os tipos móveis da gráfica
Uma tipografia não funde um bloco de metal novo para cada letra impressa no livro. Ela tem um tipo para o 'a' e o reutiliza milhares de vezes; o que muda a cada uso é a posição na página, não a letra. O molde é caro e único; a posição é barata e infinita.
O problema
Quando o número de objetos vai à casa das centenas de milhares, o que era desprezível vira decisivo. Alguns quilobytes duplicados por objeto se tornam gigabytes.
O detalhe importante é onde está o desperdício: quase nunca no que diferencia os objetos, quase sempre no que eles têm em comum.
A divisão entre intrínseco e extrínseco é a decisão de projeto do padrão, e não é óbvia. Intrínseco é o que pode ser compartilhado sem que ninguém perceba: precisa ser imutável e independente do contexto. Extrínseco é tudo o que dá identidade à instância — posição, estado, dono.
Como o objeto compartilhado não pode guardar o extrínseco, ele deixa de ser autossuficiente: as operações passam a receber por parâmetro o que antes era campo. Isso muda a assinatura dos métodos e é o principal impacto do padrão na legibilidade.
Vale distinguir de Singleton: ali existe uma instância única por decisão de projeto, para controlar acesso. Aqui existem várias instâncias compartilhadas por valor, para economizar memória — a fábrica devolve uma por espécie, não uma no total. E de cache: o objetivo do cache é evitar recomputar; o do flyweight é evitar duplicar.
Meça antes, sempre
Este é, entre os padrões do GoF, o mais claramente motivado por desempenho — e por isso o mais sujeito a ser aplicado sem necessidade.
Linguagens modernas já compartilham mais do que se imagina. Strings literais são internadas, valores pequenos são reaproveitados, e coletores de lixo compactam objetos. Boa parte do ganho que o padrão prometia em 1994 hoje vem de graça.
Antes de adotar, o número que importa é quanto da memória está em estado duplicado. Se cem mil objetos ocupam 800 MB e o intrínseco compartilhável representa 90% disso, o padrão é a resposta. Se representa 5%, ele só vai adicionar indireção e complicar a leitura.
Vale considerar alternativas antes: estruturas orientadas a dados (arrays paralelos em vez de objetos), tipos de valor, __slots__ em Python, ou simplesmente não materializar todos os objetos ao mesmo tempo. Às vezes a resposta certa não é compartilhar, é não criar.
Com Flyweight: o comum é compartilhado
- duas texturas para 100.000 árvores
- cada árvore guarda só x, y e uma referência
- memória proporcional à variedade, não à quantidade
- métodos recebem o estado extrínseco por parâmetro
O custo é legibilidade e disciplina: o flyweight precisa ser imutável, e o objeto deixa de ser autoexplicativo porque parte do seu estado vive fora dele.
FabricaTiposuma instância por espécie, no máximo
TipoArvore 'pinheiro'intrínseco — textura pesada, imutável
TipoArvore 'carvalho'intrínseco — compartilhado por 50 mil árvores
Arvore ×100.000extrínseco — só x, y e a referência ao tipo
- 1
Separar o estado
Intrínseco (igual e imutável) fica no flyweight; extrínseco (posição, dono) fica fora.
- 2
Criar pela fábrica
A fábrica devolve a mesma instância para a mesma chave — sem duplicar o pesado.
- 3
Contexto leve
Cada instância guarda só o extrínseco + a referência ao flyweight.
- 4
Passar o extrínseco
Nas operações, o contexto envia x/y (etc.) por parâmetro ao flyweight.
Navegue pelas camadas
Camada 1/3Flyweight
use ← →Camada 1
Flyweight
o estado que se repete
Guarda apenas o intrínseco e precisa ser imutável — ele é compartilhado por milhares de contextos, e uma alteração afetaria todos de uma vez. Suas operações recebem o extrínseco por parâmetro.
No código
class TipoArvore { constructor(readonly especie: string, readonly textura: string) {} }Se violar: flyweight com campo mutável faz uma árvore alterar a aparência de todas as outras da mesma espécie.
Estrutura
Código
// Flyweight: so o estado INTRINSECO (igual para milhares de arvores)
class TipoArvore {
constructor(
readonly especie: string,
readonly textura: string, // imagina alguns MB
) {}
// o estado EXTRINSECO chega por parametro
desenhar(x: number, y: number): void {
console.log(`${this.especie} em (${x},${y})`);
}
}
// Fabrica: garante uma instancia por especie
class FabricaTipos {
private cache = new Map<string, TipoArvore>();
obter(especie: string, textura: string): TipoArvore {
let t = this.cache.get(especie);
if (!t) {
t = new TipoArvore(especie, textura);
this.cache.set(especie, t);
}
return t;
}
get tamanho(): number {
return this.cache.size;
}
}
// Contexto: leve, so guarda o que e proprio + referencia ao flyweight
class Arvore {
constructor(
private x: number,
private y: number,
private tipo: TipoArvore,
) {}
desenhar(): void {
this.tipo.desenhar(this.x, this.y);
}
}
const fabrica = new FabricaTipos();
const floresta: Arvore[] = [];
for (let i = 0; i < 100_000; i++) {
const especie = i % 2 ? "pinheiro" : "carvalho";
floresta.push(new Arvore(i, i, fabrica.obter(especie, "textura...")));
}
console.log(floresta.length, "árvores usando", fabrica.tamanho, "texturas"); // 100000 / 2class TipoArvore:
"""Flyweight: so o estado INTRINSECO."""
__slots__ = ("especie", "textura")
def __init__(self, especie, textura):
self.especie, self.textura = especie, textura
# o estado EXTRINSECO chega por parametro
def desenhar(self, x, y):
print(f"{self.especie} em ({x},{y})")
class FabricaTipos:
def __init__(self):
self._cache = {}
def obter(self, especie, textura):
if especie not in self._cache:
self._cache[especie] = TipoArvore(especie, textura)
return self._cache[especie]
def __len__(self):
return len(self._cache)
class Arvore:
"""Contexto: leve, guarda so o que e proprio."""
__slots__ = ("x", "y", "tipo")
def __init__(self, x, y, tipo):
self.x, self.y, self.tipo = x, y, tipo
def desenhar(self):
self.tipo.desenhar(self.x, self.y)
fabrica = FabricaTipos()
floresta = [
Arvore(i, i, fabrica.obter("pinheiro" if i % 2 else "carvalho", "textura..."))
for i in range(100_000)
]
print(len(floresta), "arvores usando", len(fabrica), "texturas") # 100000 / 2Casos de uso
Vegetação em cenário de jogo
Cenário
Um mapa aberto tem centenas de milhares de árvores, arbustos e pedras, com poucas dezenas de modelos e texturas distintos entre eles.
Como o padrão entra
Modelo e textura viram flyweights compartilhados; cada instância guarda apenas posição, rotação e escala. É a base do rendering por instanciação nas placas de vídeo.
Trade-off: Ganha-se memória e perde-se a possibilidade de variar detalhes por instância sem quebrar o compartilhamento — dar uma cor levemente diferente a uma árvore específica exige tirá-la do grupo ou mover a cor para o extrínseco.
Glifos em editor de texto
Cenário
Um documento com um milhão de caracteres não pode ter um objeto completo por caractere, com fonte, tamanho e métricas replicados.
Como o padrão entra
Cada glifo de uma fonte é um flyweight com sua forma e métricas; o documento guarda a sequência de códigos e as posições. É o exemplo original do GoF, e continua válido.
Trade-off: Formatação por trecho (negrito numa palavra) precisa ser representada fora dos glifos, normalmente como intervalos — o que torna operações de edição no meio do texto bem mais complicadas do que seriam com objetos completos.
Metadados de eventos em telemetria
Cenário
Um coletor processa milhões de eventos por minuto, e cada um carrega nome, tipo, unidade e conjunto de rótulos que se repetem muito.
Como o padrão entra
Os descritores de métrica são internados numa tabela; cada evento guarda a referência e apenas valor e timestamp. A economia é grande porque a variedade de descritores é baixa.
Trade-off: A tabela de internamento cresce com a cardinalidade e vira vazamento se as chaves forem geradas dinamicamente (um rótulo com id de usuário, por exemplo) — o cache passa a guardar tudo para sempre.
O flyweight mutável
Compila, roda, passa no code review. E quebra depois.
O compartilhamento está lá — e o estado intrínseco ainda aceita escrita. Uma alteração num contexto pinta todos os outros da mesma chave.
class TipoArvore {
constructor(
public especie: string,
public textura: string,
public corTintura = "natural", // <- mutavel e compartilhado
) {}
pintar(cor: string) { this.corTintura = cor; }
desenhar(x: number, y: number) {
console.log(this.especie, this.corTintura, x, y);
}
}
const pinheiro = new TipoArvore("pinheiro", "tex...");
// milhares de arvores apontam para o MESMO pinheiro
pinheiro.pintar("vermelho");
// todas as arvores da especie mudaram de cor — sem aviso.O que quebra
- Na telaÁrvores 'mudam de cor sozinhas' — o sintoma clássico de estado compartilhado mutável.
- No testeUm caso pinta o tipo e o próximo herda a cor sem resetar a fábrica.
- Ao escalarQuanto mais compartilhamento, mais larga a blast radius de cada mutação.
A saída: Estado intrínseco imutável por construção (readonly, congelar, sem setters). Variação por instância (cor, escala) vai para o extrínseco — no contexto, não no flyweight.
Armadilhas
Flyweight mutável
É o erro fatal do padrão. Como o objeto é compartilhado por milhares de contextos, qualquer alteração se propaga para todos eles — e o sintoma aparece como 'algumas árvores mudaram de cor sozinhas'. O estado intrínseco precisa ser imutável por construção, não por convenção.
Cache que nunca esvazia
A fábrica guarda flyweights indefinidamente. Se a chave tiver cardinalidade alta ou vier de entrada do usuário, o cache cresce sem limite e vira vazamento de memória — justamente no padrão adotado para economizar memória. Chaves precisam ter domínio conhecido e limitado, ou o cache precisa de descarte.
Adotar sem medir
O padrão piora a legibilidade: métodos ganham parâmetros, objetos deixam de ser autossuficientes e surge uma fábrica obrigatória. Pagar esse preço sem um número que justifique é otimização prematura no sentido mais literal. Meça o consumo, identifique quanto está duplicado e só então decida.
Quando usar × quando evitar
Use quando
3- Há muitos objetos semelhantes e a memória é o gargalo medido.
- Boa parte do estado se repete e pode ser imutável.
- A identidade individual dos objetos não importa.
Evite quando
3- A quantidade de objetos é modesta.
- O estado é quase todo único por instância.
- Você ainda não mediu onde a memória está indo.
Testar o que ficou
3 armadilhas de Flyweight viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
Interning de stringRuntimes reaproveitam a mesma string literal em memória em vez de guardar mil cópias iguais.Symbol.forO registro global devolve o mesmo símbolo para a mesma chave, em qualquer parte do programa.React.memoEvita recriar a saída quando as props não mudaram — o estado extrínseco decide, o intrínseco se reaproveita.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Estado intrínseco compartilhado; extrínseco por instância.
const glyph = GlyphFactory.obter("A", fonte);O que ele cobra
1 salto a mais entre a chamada e o efeito.
- Separar estado intrínseco de extrínseco complica o modelo e é fácil de errar
- O pool de objetos compartilhados vira estado global, com todos os cuidados que isso exige
Não vale se: não há muitos objetos quase iguais consumindo memória — sem esse volume, a economia não paga a complexidade.
Onde isto aparece
Continue por aqui
- SingletonGarante que uma classe tenha uma única instância no processo e oferece um ponto global de acesso a ela — uma conveniência que cobra caro em acoplamento e testabilidade.
- ProxyColoca um substituto com a mesma interface na frente de um objeto para controlar o acesso a ele — adiando a criação, cacheando resultados, checando permissão ou falando com um objeto remoto.
- CompositeCompõe objetos em árvores e faz o cliente tratar um item isolado e um grupo inteiro exatamente do mesmo jeito, através de uma interface comum.