Padrões criacionais: Builder e Prototype
1. Introdução aos Padrões Criacionais no Contexto Arquitetural
1.1. O papel dos padrões criacionais na separação entre lógica de construção e representação
Na Arquitetura de Software, um dos princípios mais fundamentais é a separação de preocupações. Padrões criacionais como Builder e Prototype existem precisamente para isolar o como um objeto é construído do que ele representa. Essa separação permite que a lógica de montagem evolua independentemente da estrutura final do objeto, reduzindo o acoplamento entre o sistema de criação e o sistema consumidor.
1.2. Problemas comuns de acoplamento que Builder e Prototype resolvem
Dois problemas arquiteturais frequentes são:
- Construtores telescópicos: quando um objeto possui muitas variações de construção, os construtores se multiplicam exponencialmente.
- Criação custosa: objetos que exigem inicialização pesada (leitura de arquivos, consultas a banco, cálculos complexos) precisam ser reutilizados sem recriar todo o estado.
Builder resolve o primeiro problema; Prototype resolve o segundo.
1.3. Visão geral: quando cada padrão é mais adequado
| Critério | Builder | Prototype |
|---|---|---|
| Complexidade de construção | Alta (muitos passos, parâmetros opcionais) | Baixa (cópia de estado existente) |
| Custo de criação | Pode ser alto, mas controlado | Evita recriação completa |
| Variações de configuração | Muitas combinações possíveis | Poucas variações a partir de protótipos |
| Imutabilidade | Suporta objetos imutáveis | Requer cuidado com cópia rasa |
2. Builder: Construção Passo a Passo de Objetos Complexos
2.1. Conceito fundamental
Builder separa a construção de um objeto complexo de sua representação. O mesmo processo de construção pode criar diferentes representações.
2.2. Estrutura arquitetural
- Director: orquestra a sequência de passos
- Builder (interface): declara os passos de construção
- ConcreteBuilder: implementa os passos e produz o Product
- Product: o objeto final construído
2.3. Exemplo de código: construção de um objeto Documento
// Product
class Documento {
constructor() {
this.titulo = '';
this.secoes = [];
this.rodape = '';
}
}
// Builder interface (abstrato)
class DocumentoBuilder {
setTitulo(titulo) { throw new Error('Implementar'); }
adicionarSecao(titulo, conteudo) { throw new Error('Implementar'); }
setRodape(rodape) { throw new Error('Implementar'); }
build() { throw new Error('Implementar'); }
}
// ConcreteBuilder
class RelatorioBuilder extends DocumentoBuilder {
constructor() {
super();
this.reset();
}
reset() {
this.documento = new Documento();
}
setTitulo(titulo) {
this.documento.titulo = `RELATÓRIO: ${titulo}`;
return this;
}
adicionarSecao(titulo, conteudo) {
this.documento.secoes.push({ titulo, conteudo });
return this;
}
setRodape(rodape) {
this.documento.rodape = `-- ${rodape} --`;
return this;
}
build() {
const resultado = this.documento;
this.reset();
return resultado;
}
}
// Director
class DiretorDocumentos {
constructor(builder) {
this.builder = builder;
}
construirRelatorioSimples(titulo) {
return this.builder
.setTitulo(titulo)
.adicionarSecao('Introdução', 'Texto introdutório...')
.adicionarSecao('Conclusão', 'Considerações finais...')
.setRodape('Confidencial')
.build();
}
}
// Uso
const builder = new RelatorioBuilder();
const diretor = new DiretorDocumentos(builder);
const relatorio = diretor.construirRelatorioSimples('Vendas 2024');
3. Builder em Cenários Arquiteturais Reais
3.1. Aplicação em builders de consultas SQL ou APIs
// QueryBuilder para consultas SQL
class QueryBuilder {
constructor() {
this.selectClause = [];
this.fromClause = '';
this.whereClause = [];
this.orderByClause = [];
}
select(...campos) {
this.selectClause.push(...campos);
return this;
}
from(tabela) {
this.fromClause = tabela;
return this;
}
where(condicao) {
this.whereClause.push(condicao);
return this;
}
orderBy(campo, direcao = 'ASC') {
this.orderByClause.push(`${campo} ${direcao}`);
return this;
}
build() {
const select = this.selectClause.length > 0
? this.selectClause.join(', ')
: '*';
const where = this.whereClause.length > 0
? `WHERE ${this.whereClause.join(' AND ')}`
: '';
const orderBy = this.orderByClause.length > 0
? `ORDER BY ${this.orderByClause.join(', ')}`
: '';
return `SELECT ${select} FROM ${this.fromClause} ${where} ${orderBy}`.trim();
}
}
// Uso
const query = new QueryBuilder()
.select('nome', 'email')
.from('usuarios')
.where('ativo = true')
.where('data_cadastro > "2024-01-01"')
.orderBy('nome')
.build();
3.2. Uso em frameworks de UI para construção de componentes aninhados
Frameworks como Flutter e React utilizam variações do Builder para montar árvores de componentes de forma declarativa.
3.3. Variação: Fluent Builder e imutabilidade
O Fluent Builder encadeia chamadas de método retornando this, permitindo uma sintaxe fluente. Quando combinado com imutabilidade, cada método retorna um novo builder com o estado atualizado, garantindo que o builder original não seja modificado.
4. Prototype: Clonagem como Estratégia de Criação
4.1. Conceito fundamental
Prototype permite criar novos objetos copiando um protótipo existente, em vez de instanciar uma classe do zero. Isso é útil quando a criação de um objeto é mais cara que a cópia.
4.2. Estrutura arquitetural
- Prototype (interface): declara o método
clone() - ConcretePrototype: implementa a clonagem
- Cliente: usa o método
clone()para criar novos objetos
4.3. Exemplo de código: clonagem de ConfiguracaoSistema
// Prototype interface
class Prototype {
clone() {
throw new Error('Método clone deve ser implementado');
}
}
// ConcretePrototype
class ConfiguracaoSistema extends Prototype {
constructor(nome, versao, parametros) {
super();
this.nome = nome;
this.versao = versao;
this.parametros = parametros || {}; // objeto mutável
this.dataCriacao = new Date();
}
// Cópia rasa (shallow)
clone() {
const copia = Object.create(Object.getPrototypeOf(this));
copia.nome = this.nome;
copia.versao = this.versao;
copia.parametros = { ...this.parametros }; // cópia superficial do objeto
copia.dataCriacao = new Date(this.dataCriacao);
return copia;
}
// Cópia profunda (deep) - necessária para objetos aninhados
cloneDeep() {
return JSON.parse(JSON.stringify(this));
}
toString() {
return `Config[${this.nome} v${this.versao}, params: ${JSON.stringify(this.parametros)}]`;
}
}
// Uso
const configOriginal = new ConfiguracaoSistema(
'ServidorPro',
'2.1.0',
{ timeout: 5000, maxConexoes: 100, debug: false }
);
const configClone = configOriginal.clone();
configClone.parametros.timeout = 10000; // modifica apenas o clone
const configDeepClone = configOriginal.cloneDeep();
5. Prototype em Cenários Arquiteturais Reais
5.1. Aplicação em sistemas de cache e objetos de alto custo de inicialização
// Registry de protótipos para objetos caros de criar
class PrototypeRegistry {
constructor() {
this.prototipos = new Map();
}
registrar(nome, prototipo) {
this.prototipos.set(nome, prototipo);
}
criar(nome) {
const prototipo = this.prototipos.get(nome);
if (!prototipo) {
throw new Error(`Prototype '${nome}' não encontrado`);
}
return prototipo.clone();
}
}
// Uso em sistema de cache de configurações
const registry = new PrototypeRegistry();
registry.registrar('config_padrao', new ConfiguracaoSistema('Padrão', '1.0', { timeout: 3000 }));
registry.registrar('config_producao', new ConfiguracaoSistema('Produção', '1.0', { timeout: 10000 }));
const novaConfig = registry.criar('config_producao');
novaConfig.versao = '1.1'; // personaliza sem afetar o protótipo
5.2. Uso em editores gráficos ou jogos para duplicação de entidades complexas
Em engines de jogos, cada entidade (inimigo, item, personagem) pode ser um protótipo. Criar um novo inimigo a partir de um template clonado é muito mais rápido que carregar texturas, animações e scripts do zero.
5.3. Desafios: referências circulares e cópia profunda
Linguagens como JavaScript e Python exigem implementação manual de clonagem profunda. Referências circulares podem causar loops infinitos. Soluções incluem:
- Usar JSON.parse(JSON.stringify(obj)) (não funciona com funções ou referências circulares)
- Implementar um clonador customizado com mapa de objetos já visitados
- Utilizar bibliotecas como lodash.cloneDeep
6. Comparação Arquitetural: Builder vs. Prototype
6.1. Diferenças fundamentais
| Aspecto | Builder | Prototype |
|---|---|---|
| Estratégia | Construção passo a passo | Clonagem de estado |
| Controle | Director controla a ordem | Cliente escolhe o protótipo |
| Complexidade | Média a alta (muitos passos) | Baixa a média (clonagem) |
| Imutabilidade | Fácil de garantir | Requer cuidado |
6.2. Impacto no acoplamento e na flexibilidade
Builder reduz o acoplamento entre a lógica de construção e o produto final. Prototype reduz o acoplamento entre a criação e a configuração do objeto, mas aumenta o acoplamento com o estado interno.
6.3. Trade-offs de desempenho, memória e complexidade de manutenção
- Builder: maior overhead de criação (objetos intermediários), mas mais fácil de manter e estender.
- Prototype: criação muito rápida, mas manutenção complexa se a clonagem não for bem implementada (cópias rasas vs. profundas).
7. Integração com Outros Padrões e Boas Práticas
7.1. Combinação Builder + Abstract Factory
Abstract Factory pode fornecer builders específicos para diferentes famílias de produtos. Por exemplo, uma fábrica abstrata de documentos pode retornar um RelatorioBuilder ou um ContratoBuilder, ambos implementando a mesma interface de construção.
7.2. Combinação Prototype + Registry
O Prototype Registry (visto no exemplo da seção 5.1) centraliza o gerenciamento de protótipos, permitindo que clientes criem objetos sem conhecer suas classes concretas.
7.3. Relação com princípios SOLID
- Responsabilidade Única: Builder isola a lógica de construção; Prototype isola a lógica de clonagem.
- Inversão de Dependência: Ambos dependem de abstrações (interfaces), não de implementações concretas.
- Aberto/Fechado: É possível adicionar novos builders ou protótipos sem modificar o código existente.
8. Conclusão e Recomendações Arquiteturais
8.1. Resumo dos pontos-chave para decisão arquitetural
- Use Builder quando: o objeto tem muitos parâmetros opcionais, a construção exige validação passo a passo, ou você precisa de diferentes representações do mesmo processo.
- Use Prototype quando: a criação do objeto é cara (I/O, rede, computação), você precisa de muitas variações similares, ou o sistema exige duplicação em tempo de execução.
8.2. Armadilhas comuns
- Overengineering: não use Builder para objetos simples com 2-3 parâmetros.
- Complexidade desnecessária: Prototype com clonagem profunda em objetos simples pode ser substituído por construtores com parâmetros.
- Falta de documentação: ambos os padrões exigem documentação clara sobre os passos de construção ou a profundidade da clonagem.
8.3. Recomendações finais
Em arquiteturas modernas, Builder é preferível para APIs públicas e objetos de configuração. Prototype é ideal para sistemas que gerenciam grande volume de objetos similares (jogos, simuladores, caches). Avalie sempre o custo-benefício antes de implementar.
Referências
- Builder Pattern - Refactoring Guru — Explicação detalhada com exemplos em múltiplas linguagens e diagramas UML.
- Prototype Pattern - Refactoring Guru — Guia completo sobre clonagem, cópia rasa vs. profunda, e exemplos práticos.
- Design Patterns: Elements of Reusable Object-Oriented Software (GoF) — Livro clássico que definiu os padrões Builder e Prototype.
- Builder Pattern in Java - Baeldung — Tutorial prático com implementação em Java, incluindo Fluent Builder.
- Prototype Pattern in JavaScript - DigitalOcean — Exemplo focado em JavaScript com clonagem e registry.
- Martin Fowler - Patterns of Enterprise Application Architecture — Contexto arquitetural para padrões criacionais em sistemas empresariais.
- Builder vs Prototype - Stack Overflow Discussion — Discussão técnica sobre quando usar cada padrão com exemplos reais.