Padrão de Criação (GoF)

Prototype

Especifica os tipos de objetos a criar usando uma instância prototípica e cria novos objetos por clonagem desse protótipo — evitando a instanciação via new e permitindo criar variantes a partir de um estado base pré-configurado.

Intenção

Criar novos objetos clonando uma instância existente (o protótipo) em vez de instanciar via new. O código cliente pede ao protótipo que se clone — sem precisar conhecer a classe concreta do objeto sendo criado.

Catalogado por Gamma, Helm, Johnson e Vlissides no livro Design Patterns: Elements of Reusable Object-Oriented Software (1994), o Prototype pertence à categoria de padrões de criação. É útil quando a construção de um objeto do zero é custosa ou complexa, quando você quer criar variações a partir de um estado base pré-configurado, ou quando a classe concreta só é conhecida em tempo de execução.

Problema

Imagine um sistema que precisa criar variações de uma configuração de serviço. Cada variante (staging, produção, teste de carga) compartilha um conjunto grande de parâmetros base e difere apenas em alguns campos. Criar cada variante do zero — repetindo todos os parâmetros comuns — é tedioso, propenso a erros e viola o princípio DRY.

Uma solução ingênua é copiar e colar a instanciação, mas isso espalha parâmetros pelo código. Outra é um Factory Method para cada variante, mas se as variantes são configuradas dinamicamente em runtime, o número de factories pode explodir.

O Prototype resolve: você cria um objeto base (o protótipo) totalmente configurado, registra-o num catálogo e, quando precisa de uma variação, clona o protótipo mais próximo e ajusta apenas o que difere.

Prototype vs Factory

  • Factory Method / Abstract Factory criam objetos do zero, instanciando as classes concretas a cada chamada. A lógica de configuração vive na fábrica.
  • Prototype cria objetos copiando uma instância existente já configurada. A lógica de configuração está no estado do protótipo — útil quando a criação envolve estado acumulado que seria difícil de reproduzir a cada vez.

Solução

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

  1. Prototype (interface): declara o método de clonagem, geralmente chamado clone(). Todas as classes clonáveis implementam essa interface.
  2. ConcretePrototype: implementa o método clone(), criando uma cópia de si mesmo. É responsabilidade do ConcretePrototype garantir que a cópia seja profunda (deep copy) quando necessário — este é o ponto mais crítico do padrão.
  3. Prototype Registry (opcional): um catálogo de protótipos pré-configurados. O cliente solicita um clone pelo nome, sem precisar saber qual classe concreta está por trás. Substitui o Factory Method quando os tipos são configurados em runtime.

O ponto mais crítico na implementação é a distinção entre cópia rasa (shallow copy) e cópia profunda (deep copy). Uma cópia rasa copia apenas as referências para objetos aninhados — o clone e o original passam a compartilhar os mesmos sub-objetos, e uma modificação num afeta o outro. Uma cópia profunda recria cada objeto aninhado de forma independente.

Estrutura

            «interface»
             Clonavel
  ┌──────────────────────────┐
  │ + clone(): Clonavel      │
  └──────────────────────────┘
              ▲
    ┌─────────┴──────────────────────┐
    │                                │
ConfigTemplate                 ConfigPremium
(ConcretePrototype)            (ConcretePrototype)
clone()                        clone()
  → new ConfigTemplate(...)      → new ConfigPremium(...)
  // deep copy de campos          // deep copy de campos


            PrototypeRegistry
  ┌──────────────────────────────────────────────────┐
  │ - catalogo: Map<string, Clonavel>                │
  │ + registrar(chave: string, proto: Clonavel): void│
  │ + criar(chave: string): Clonavel                 │
  └──────────────────────────────────────────────────┘
              │
              │  catalogo.get(chave).clone()
              ▼
     novo objeto independente (clone do protótipo)

Fluxo:

  registry.registrar("producao", configBase)
  const serv = registry.criar("producao")  // retorna clone
  serv.nome = "pagamentos"                 // não afeta o protótipo

Exemplos de código

Exemplo 1 — Shallow copy vs Deep copy

A armadilha central do Prototype: uma cópia rasa compartilha referências para objetos aninhados entre o clone e o original, causando efeitos colaterais inesperados. O método clone() deve garantir cópia profunda de todas as propriedades que são referências.

// Em TypeScript, arrays e objetos são passados por referência.
// Uma cópia rasa copia apenas o ponteiro — clone e original compartilham
// o mesmo objeto/array subjacente.

class ConfigTemplate {
  constructor(
    public nome: string,
    public timeoutMs: number,
    public tags: string[],                  // array — referência compartilhada em cópia rasa
    public headers: Record<string, string>, // objeto — referência compartilhada em cópia rasa
  ) {}

  // ── ERRADO: cópia rasa via Object.assign ─────────────────
  // Object.assign copia apenas o primeiro nível: tags e headers ainda
  // apontam para os MESMOS objetos da instância original.
  cloneRaso(): ConfigTemplate {
    return Object.assign(
      new ConfigTemplate("", 0, [], {}),
      this,
    );
  }

  // ── Cópia suficiente: 1 nível basta porque os valores são primitivos ──
  // tags: array de strings (primitivos) → spread garante independência total.
  // headers: objeto cujos valores são strings (primitivos) → spread de 1 nível é seguro.
  // Se os valores fossem objetos aninhados, seria preciso cloná-los recursivamente.
  clone(): ConfigTemplate {
    return new ConfigTemplate(
      this.nome,
      this.timeoutMs,
      [...this.tags],           // novo array independente
      { ...this.headers },      // novo objeto independente (valores são primitivos)
    );
  }
}

// ── Demonstração do problema ──────────────────────────────────
const base = new ConfigTemplate(
  "servico-base",
  5000,
  ["producao", "critico"],
  { "X-Api-Key": "chave-base" },
);

// Cópia rasa — PERIGOSO:
const raso = base.cloneRaso();
raso.tags.push("debug");               // modifica o array DO BASE!
raso.headers["X-Debug"] = "true";     // modifica o objeto DO BASE!
console.log(base.tags);               // ["producao", "critico", "debug"] ← contaminado
console.log("X-Debug" in base.headers); // true ← contaminado

// Cópia profunda — CORRETO:
const profundo = base.clone();
profundo.nome = "servico-staging";
profundo.tags.push("verbose");
profundo.headers["X-Env"] = "staging";

console.log(base.nome);               // "servico-base"    ← inalterado
console.log(base.tags);               // ["producao", "critico", "debug"] (do raso acima)
console.log("X-Env" in base.headers); // false             ← clone profundo não contaminou

Exemplo 2 — Prototype Registry

O Registry mantém um catálogo de protótipos pré-configurados. O cliente solicita um clone pelo nome — sem conhecer a classe concreta por trás. Funciona como um Factory que cria por clonagem em vez de instanciação.

// ── Prototype Registry ────────────────────────────────────────
// Catálogo de protótipos pré-configurados.
// O cliente recebe sempre um CLONE — nunca o protótipo original.
class PrototypeRegistry {
  private readonly catalogo = new Map<string, ConfigTemplate>();

  registrar(chave: string, proto: ConfigTemplate): void {
    this.catalogo.set(chave, proto);
  }

  criar(chave: string): ConfigTemplate {
    const proto = this.catalogo.get(chave);
    if (!proto) {
      throw new Error(`Protótipo não encontrado: "${chave}".`);
    }
    return proto.clone(); // retorna CLONE — protótipo original nunca é exposto
  }
}

// ── Registrando protótipos pré-configurados ───────────────────
const registry = new PrototypeRegistry();

registry.registrar("producao", new ConfigTemplate(
  "producao",
  3000,
  ["critico", "monitorado"],
  { "X-Api-Key": "prod-key", "X-Env": "prod" },
));

registry.registrar("staging", new ConfigTemplate(
  "staging",
  10000,
  ["debug", "verbose"],
  { "X-Api-Key": "stg-key", "X-Env": "staging" },
));

// ── Criando variações por clonagem ────────────────────────────
const servA = registry.criar("producao");
servA.nome = "pagamentos";           // customiza o clone, não o protótipo

const servB = registry.criar("producao"); // outro clone independente
servB.nome = "usuarios";
servB.tags.push("auditoria");

console.log(servA.nome);              // "pagamentos"
console.log(servB.nome);              // "usuarios"
console.log(servA.tags.join(", "));   // "critico, monitorado"
console.log(servB.tags.join(", "));   // "critico, monitorado, auditoria"
// Os dois clones são independentes entre si e do protótipo original.

Quando usar

  • Quando a construção do zero é custosa ou complexa: se inicializar um objeto envolve consultas a banco, leituras de arquivo ou cálculos pesados, clonar um protótipo já pronto é mais eficiente.
  • Quando você precisa de variações a partir de um estado base: o protótipo carrega a configuração comum; o clone recebe apenas os ajustes específicos daquela variante.
  • Quando a classe concreta não é conhecida em tempo de compilação: o cliente chama clone() na interface — sem precisar conhecer a classe concreta do objeto sendo copiado.
  • Como alternativa ao Factory Method para tipos configurados em runtime: o Prototype Registry substitui uma hierarquia de factories quando os tipos são definidos por configuração, não por código.

Quando evitar

  • Quando a cópia profunda é complexa demais: objetos com grafos de referências circulares ou hierarquias profundas tornam a implementação correta de clone() muito difícil de manter.
  • Quando a instanciação direta é simples: se criar o objeto do zero é trivial e rápido, a clonagem adiciona complexidade sem benefício.
  • Quando os objetos têm estado que não deve ser copiado: conexões de banco, descritores de arquivo e handles de sistema operacional não fazem sentido ser clonados — clonar um objeto com esses recursos pode gerar comportamento indefinido.

Prós e contras

Prós

  • Cria objetos sem acoplar o cliente à classe concreta — o cliente só conhece a interface Clonavel.
  • Elimina código de inicialização repetido quando muitos objetos compartilham a mesma configuração base.
  • Permite criar novos objetos com estado pré-construído complexo que seria difícil de replicar via construtor.
  • O Prototype Registry funciona como um factory configurável em runtime — novos tipos podem ser registrados sem recompilar.

Contras

  • Implementar clone() corretamente (deep copy) pode ser difícil para objetos com hierarquias complexas ou referências circulares.
  • Em PHP, cada classe que precisa de deep copy deve implementar __clone() explicitamente — fácil de esquecer em classes aninhadas.
  • Clonar objetos com recursos externos (conexões, handles) pode causar comportamento inesperado se esses recursos não forem tratados no clone.

Armadilhas comuns

1. Cópia rasa (shallow copy) — a armadilha central

Atenção: Esta é a armadilha mais frequente e mais sutil do Prototype. Uma cópia rasa copia apenas as referências de primeiro nível — objetos e arrays aninhados continuam sendo compartilhados entre o clone e o original. Modificar o array tags no clone, por exemplo, altera o protótipo. Sempre implemente clone() com cópia profunda para cada propriedade que é uma referência.

Em TypeScript: use spread ({ ...obj }, [...arr]) para um nível quando os valores são primitivos — é o caso mais comum e mais seguro. Para sub-grafos de dados puros arbitrariamente profundos (sem instâncias de classes), structuredClone() é uma opção conveniente, mas com ressalvas importantes: ela preserva ciclos e profundidade, porém descarta o prototype chain — o resultado não é instância da classe original, perdendo métodos como clone(). Além disso, lança DataCloneError se o grafo contiver funções ou objetos não-clonáveis (como Map com valores de classe). Para clonar instâncias preservando a classe, faça o deep copy campo a campo dentro do próprio clone(), usando structuredClone() apenas nos sub-grafos de dados puros que não contêm instâncias de classes. Em PHP: implemente __clone() e clone explicitamente cada propriedade-objeto — lembre-se de que __clone() deve ser implementado em todas as classes da hierarquia que têm sub-objetos.

2. Referências circulares em deep copy

Quando o grafo de objetos contém ciclos (A referencia B que referencia A), uma deep copy recursiva ingênua entra em loop infinito. A solução é manter um mapa de objetos já visitados durante a clonagem: se o objeto já foi clonado, retorna o clone existente em vez de recriar. Note que, embora structuredClone() lide com ciclos em dados puros, ela não serve para grafos de instâncias de classes (lança DataCloneError e perde o prototype). Para esses casos, implemente o controle de ciclos manualmente dentro do clone(), passando um Map de objetos já visitados como parâmetro auxiliar.

3. Clonar objetos com recursos externos

Conexões de banco de dados, sockets, descritores de arquivo e mutexes não devem ser clonados. Se o seu objeto possui esses recursos, o __clone() (PHP) ou o método clone() (TypeScript) deve fechar o recurso no clone e criar um novo — ou lançar uma exceção indicando que o objeto não é clonável.

4. Confundir Prototype com cópia de estado acidental

O Prototype é um padrão intencional: você cria um protótipo propositalmente para servir de base para clones. Se você está clonando objetos apenas para evitar escrever o construtor, reconsidere — um Builder com valores padrão pode ser mais explícito e seguro, pois centraliza a lógica de criação sem o risco de vazar estado do protótipo.

Padrões relacionados

O Prototype se conecta diretamente aos outros padrões de criação:

O Abstract Factory pode usar o Prototype internamente: em vez de criar produtos do zero, a fábrica concreta clona protótipos registrados — reduzindo o número de subclasses necessárias. O Builder e o Prototype são alternativas para criar objetos complexos com estado inicial pré-definido: o Builder monta o objeto passo a passo com validação centralizada; o Prototype copia um objeto já montado e pronto. Escolha Builder quando o processo de montagem importa; escolha Prototype quando o estado inicial já existe e precisa ser replicado com variações.