CriacionalIntermediário6 min de leitura8 seções1994, Marco de referência: a ideia é mais velha que a data

Builder

Constrói objetos complexos passo a passo por uma interface fluente, separando a montagem (incremental, com escolhas opcionais) do objeto final (completo e validado).

Em 10 segundos

Monte objetos complexos passo a passo: cada método do builder configura uma parte e devolve o próprio builder; construir() valida tudo em um ponto único e entrega o objeto pronto — adeus construtor de oito parâmetros posicionais.

  • #gof
  • #fluent-interface
  • #imutabilidade
  • #telescoping-constructor
Analogia

O balcão de sanduíches

Você não entrega uma lista de doze ingredientes de uma vez ao atendente — escolhe o pão, depois a proteína, pula o que não quer, acrescenta extras. Cada escolha é opcional e tem nome claro. O atendente (builder) acumula os passos e monta na ordem certa; o sanduíche só existe quando você diz 'pode fechar'. Ninguém recebe um sanduíche pela metade nem precisa decorar em que posição da lista vai o queijo.

01Conceito

O problema

Objetos com muitos parâmetros opcionais empurram o código para o telescoping constructor: uma escada de construtores sobrecarregados ou chamadas como new Pedido(itens, null, true, null), em que ninguém sabe o que cada posição significa.

A alternativa ingênua — criar vazio e ir preenchendo com setters — deixa o objeto existir em estados intermediários inválidos, e a validação se espalha por todos os pontos de uso.

O nome 'telescoping constructor' vem da forma: construtor de 2 parâmetros chama o de 3, que chama o de 4, como um telescópio se abrindo. Cada combinação de opcionais pede mais uma sobrecarga, e o chamador acaba passando null/false posicionais para alcançar o parâmetro que interessa. É o cheiro clássico que o Builder resolve — popularizado nessa forma fluente por Joshua Bloch no Effective Java.

Vale distinguir as duas encarnações do padrão. O Builder clássico do GoF separa construção de representação: um Director executa os mesmos passos sobre builders diferentes para produzir produtos diferentes (o mesmo roteiro gera relatório HTML ou PDF). O builder fluente do dia a dia ataca outro problema — legibilidade e validação de objetos com muitos opcionais — e raramente tem Director. Os dois compartilham o mecanismo: acumular estado parcial e entregar o produto no final.

Honestidade necessária: em linguagens com parâmetros nomeados e valores default (Python, Kotlin, C#) ou objeto literal tipado (TypeScript), boa parte dos casos dispensa builder — new Pedido({ itens, embrulho: true }) já é legível. O builder volta a valer a pena quando há validação entre campos, passos que precisam de ordem, ou quando você quer imutabilidade com montagem incremental.

Como se encaixa
  1. Cliente
  2. encadeia passos com nomeBuilder
  3. acumula escolhasEstado parcial
  4. construir() valida e montaObjeto completo
As escolhas se acumulam no builder — o objeto final só passa a existir depois que construir() valida tudo de uma vez.
02Anatomia

Navegue pelas camadas

Camada 1/4Cliente

Camada 1

Cliente

encadeia só os passos que precisa

Lê como uma frase: cada passo tem nome de negócio (comCupom, paraPresente) e os opcionais simplesmente não aparecem. A ordem dos passos costuma ser livre — quem garante a coerência é o construir(), não o chamador.

No código

new PedidoBuilder()
  .adicionarItem("Camiseta P")
  .paraPresente()

Se violar: cliente que monta o objeto por setters depois do construir() reabre a janela de estados inválidos que o builder fechou.

03Estrutura

Estrutura

Diagrama de classes
04Implementação

Código

class Pedido {
  constructor(
    readonly itens: string[],
    readonly cupom?: string,
    readonly embrulho = false,
  ) {}
}
// Sem builder: new Pedido(itens, undefined, true)
// — quem lê não sabe o que o "true" significa

class PedidoBuilder {
  private itens: string[] = [];
  private cupom?: string;
  private embrulho = false;

  adicionarItem(item: string): this {
    this.itens.push(item);
    return this;                    // fluent: cada passo devolve o builder
  }
  comCupom(codigo: string): this {
    this.cupom = codigo;
    return this;
  }
  paraPresente(): this {
    this.embrulho = true;
    return this;
  }
  construir(): Pedido {
    if (this.itens.length === 0) throw new Error("Pedido sem itens");
    return new Pedido([...this.itens], this.cupom, this.embrulho);
  }
}

const pedido = new PedidoBuilder()
  .adicionarItem("Camiseta P")
  .comCupom("BEMVINDO10")
  .paraPresente()
  .construir();                     // só aqui o Pedido passa a existir
console.log(pedido.embrulho);       // true — e o nome do passo explica o porquê
05No mundo real

Casos de uso

Cobrança Pix com juros, multa e desconto

Cenário

Uma fintech emite cobranças com vencimento em que quase tudo é opcional — juros, multa, desconto por antecipação — mas as combinações têm regras: desconto só antes do vencimento, multa só depois.

Como o padrão entra

Um CobrancaBuilder expõe um passo nomeado por campo opcional e o construir() valida as combinações antes de chamar o provedor. O código de emissão lê como a regra de negócio, e cobrança inválida nem chega a existir como objeto.

Trade-off: O builder vira o guardião das regras de combinação — quando a API do provedor muda uma regra, é preciso lembrar que a validação vive no builder, não só no contrato externo.

Query builder de relatórios

Cenário

Um SaaS de analytics monta consultas com filtros, ordenação, agrupamento e paginação opcionais — é o mesmo problema que Knex e o construtor de queries do Prisma resolvem em produção.

Como o padrão entra

Cada tela encadeia só os passos que usa (porPeriodo, agrupadoPor, ordenadoPor) e o construir() gera o SQL/objeto de consulta coerente. Combinações inválidas (agrupamento sem métrica) falham na montagem, não no banco.

Trade-off: O builder garante consulta sintaticamente válida, não consulta boa: continua fácil encadear passos que geram um plano de execução péssimo — ele não substitui a revisão do SQL gerado.

Test data builders na suíte de testes

Cenário

Os testes de um e-commerce precisam de pedidos em dezenas de variações — pago, cancelado, com N itens, com cupom — e montar cada um na mão duplica detalhes irrelevantes em todo teste.

Como o padrão entra

Um umPedido() com defaults válidos e passos como .pago() e .comItens(3) deixa cada teste declarar apenas o que importa para o cenário. Mudou o modelo? Ajusta-se o builder, não os duzentos testes.

Trade-off: Os defaults escondidos podem mascarar o que o teste realmente exige — um teste que passa por causa de um default do builder é um teste que mente sobre o próprio cenário.

Passo a passoComo montar sem nascer pela metade
  1. 1

    Acumular no builder

    Campos privados mutáveis e métodos fluentes que devolvem `this`. Só o builder pode estar 'pela metade'.

  2. 2

    Validar em construir()

    Invariantes que cruzam campos (itens não vazios, desconto compatível com vencimento) falham aqui — ou o objeto não nasce.

  3. 3

    Entregar imutável

    O produto recebe tudo no construtor e não expõe setters. Quem tem um `Pedido` tem a garantia da validação.

  4. 4

    Um builder por produto

    Reusar a mesma instância após `construir()` vaza cupom e itens do pedido anterior. Crie outro ou resete com clareza.

O Builder que não valida e entrega mutável

Compila, roda, passa no code review. E quebra depois.

A API é fluente e o nome é Builder — mas construir() aceita qualquer coisa e o produto ainda tem addItem. A montagem bonita não fechou a janela de estados inválidos.

class PedidoBuilder {
  private itens: string[] = [];
  private cupom?: string;

  adicionarItem(item: string): this {
    this.itens.push(item);
    return this;
  }
  comCupom(codigo: string): this {
    this.cupom = codigo;
    return this;
  }

  // "Builder"... sem validacao e com produto mutavel.
  construir(): Pedido {
    return new Pedido(this.itens, this.cupom); // pedido vazio passa
  }
}

class Pedido {
  constructor(public itens: string[], public cupom?: string) {}
  addItem(i: string) { this.itens.push(i); } // <- reabre estado invalido
}

const p = new PedidoBuilder().construir(); // Pedido sem itens
p.addItem("camiseta");                     // invariante validada? nunca.

O que quebra

  • Com zero passosUm pedido sem itens circula no sistema; o erro aparece longe, no gateway ou no PDF.
  • Depois do construirQualquer código muta o produto e as invariantes 'validadas' deixam de ser garantia.
  • Na leituraNinguém sabe se precisa passar pelo builder ou se pode new Pedido([]) direto — dois caminhos, duas regras.

A saída: construir() recusa pedido vazio; o produto nasce com readonly e sem setters. O builder é o único caminho de criação — senão é só açúcar sintático.

07Cuidado

Armadilhas

  1. construir() que não valida

    Se qualquer sequência de passos produz um objeto — inclusive um quebrado —, o builder é só sintaxe bonita em volta do mesmo bug. As invariantes que cruzam campos (itens não vazios, desconto compatível com vencimento) pertencem ao construir(); as de campo único podem falhar já no próprio passo.

  2. Builder reutilizado entre produtos

    Chamar construir() duas vezes na mesma instância vaza estado do primeiro objeto para o segundo — o cupom do pedido anterior aparece no próximo. Crie um builder por produto, ou faça o construir() resetar/invalidar a instância explicitamente.

  3. Builder para objeto de três campos

    Em TypeScript, new Pedido({ itens, embrulho: true }) com um objeto literal tipado dá legibilidade de graça; em Python, parâmetros nomeados idem. Builder ali é uma classe inteira para resolver um problema que a linguagem já resolveu — reserve-o para quando houver validação entre passos ou montagem incremental de verdade.

08Decisão

Quando usar × quando evitar

Use quando

4
  • O objeto tem muitos parâmetros opcionais ou combináveis e os construtores viraram uma escada ilegível.
  • A construção exige validação entre campos ou passos ordenados antes de o objeto poder existir.
  • Você quer um produto imutável com montagem incremental (dados chegam aos poucos).
  • Os mesmos passos de construção produzem representações diferentes (Director + builders distintos).

Evite quando

3
  • Objetos com dois ou três campos autoexplicativos — cerimônia pura.
  • A linguagem resolve com parâmetros nomeados/default ou objeto literal tipado e não há validação entre passos.
  • O 'builder' seria só um espelho de setters sem validação — um objeto de configuração comunica melhor.

Testar o que ficou

3 armadilhas de Builder 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.

  • Query builders (Knex, Prisma).where().orderBy().limit() monta a consulta por partes e só no .then() produz o SQL final.
  • URLSearchParamsVocê acrescenta parâmetro a parâmetro e o toString() no fim entrega a query string pronta.
  • Request do fetchMétodo, headers, body e sinal vão sendo compostos até virar um objeto imutável de requisição.

O padrão em uma linha

Sem nome de domínio, sem cerimônia. Só a ideia.

// Monta objeto complexo passo a passo.
const url = new URLSearchParams().set("q", termo).set("page", "1");

O que ele cobra

1 salto a mais entre a chamada e o efeito.

  • Um objeto builder a manter em paralelo ao objeto que ele constrói
  • A construção passa a ser uma sequência de chamadas em vez de um construtor só

Não vale se: o objeto tem poucos campos e um construtor legível já resolve — o builder só compensa quando a montagem é complexa ou muito opcional.

Onde isto aparece

Continue por aqui