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:
-
Prototype (interface): declara o método de clonagem,
geralmente chamado
clone(). Todas as classes clonáveis implementam essa interface. -
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. - 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
<?php
// Em PHP, arrays são COPIADOS POR VALOR — `clone` já os duplica automaticamente.
// O perigo está em propriedades que são OBJETOS: sem __clone(), o clone e o
// original compartilham a mesma instância do objeto aninhado.
class ConexaoConfig
{
public function __construct(
public string $host,
public int $porta,
) {}
}
class ConfigTemplate
{
/** @param string[] $tags */
public function __construct(
public string $nome,
public int $timeoutMs,
public array $tags, // array — PHP copia por VALOR ao clonar: seguro
public ConexaoConfig $conexao, // objeto — precisa de clone explícito
) {}
// __clone() é invocado automaticamente pelo operador `clone`.
// Sem ele, $this->conexao seria a MESMA instância que o original.
public function __clone(): void
{
// $tags: array é copiado por valor — nenhuma ação necessária.
// $conexao: objeto precisa ser clonado para garantir independência.
$this->conexao = clone $this->conexao;
}
}
// ── Demonstração do problema SEM __clone ──────────────────────
class ConfigSemDeepClone
{
public function __construct(
public string $nome,
public ConexaoConfig $conexao,
) {}
// Sem __clone: operador `clone` não aprofunda para propriedades-objeto.
}
$baseSem = new ConfigSemDeepClone('base', new ConexaoConfig('localhost', 5432));
$rasoCopy = clone $baseSem;
$rasoCopy->conexao->host = 'producao.db'; // modifica o ORIGINAL!
echo $baseSem->conexao->host . PHP_EOL; // "producao.db" ← contaminado!
// ── Demonstração COM __clone (ConfigTemplate) ─────────────────
$base = new ConfigTemplate(
'servico-base',
5000,
['producao', 'critico'],
new ConexaoConfig('localhost', 5432),
);
$copia = clone $base; // __clone() é chamado automaticamente
$copia->nome = 'servico-staging';
$copia->tags[] = 'verbose'; // array: copiado por valor — seguro
$copia->conexao->host = 'staging.db'; // objeto: deep copy — seguro
echo $base->nome . PHP_EOL; // "servico-base" ← inalterado
echo implode(', ', $base->tags) . PHP_EOL; // "producao, critico" ← inalterado
echo $base->conexao->host . PHP_EOL; // "localhost" ← inalterado
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.
<?php
// ── Prototype Registry ────────────────────────────────────────
class PrototypeRegistry
{
/** @var array<string, ConfigTemplate> */
private array $catalogo = [];
public function registrar(string $chave, ConfigTemplate $proto): void
{
$this->catalogo[$chave] = $proto;
}
// Retorna sempre um CLONE — o protótipo original permanece intacto.
public function criar(string $chave): ConfigTemplate
{
if (!isset($this->catalogo[$chave])) {
throw new \InvalidArgumentException(
"Protótipo não encontrado: \"{$chave}\"."
);
}
return clone $this->catalogo[$chave]; // __clone() garante deep copy
}
}
// ── Registrando protótipos pré-configurados ───────────────────
$registry = new PrototypeRegistry();
$registry->registrar('producao', new ConfigTemplate(
'producao',
3000,
['critico', 'monitorado'],
new ConexaoConfig('db.prod.exemplo.com', 5432),
));
$registry->registrar('staging', new ConfigTemplate(
'staging',
10000,
['debug', 'verbose'],
new ConexaoConfig('db.staging.exemplo.com', 5432),
));
// ── Criando variações por clonagem ────────────────────────────
$servA = $registry->criar('producao');
$servA->nome = 'pagamentos'; // customiza o clone, não o protótipo
$servB = $registry->criar('producao'); // outro clone independente
$servB->nome = 'usuarios';
$servB->tags[] = 'auditoria';
echo $servA->nome . PHP_EOL; // "pagamentos"
echo $servB->nome . PHP_EOL; // "usuarios"
echo implode(', ', $servA->tags) . PHP_EOL; // "critico, monitorado"
echo implode(', ', $servB->tags) . PHP_EOL; // "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.