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