Padrão de Criação (GoF)

Builder

Separa a construção de um objeto complexo da sua representação, permitindo que o mesmo processo de construção crie diferentes representações e garantindo que o produto só seja entregue em estado válido.

Intenção

Separar o processo de montagem de um objeto complexo da sua representação final, permitindo que o mesmo processo de construção produza representações diferentes. O Builder encapsula cada passo em métodos individuais e centraliza toda a validação no método build(), que só entrega o produto quando a configuração está completa e consistente.

Catalogado por Gamma, Helm, Johnson e Vlissides no livro Design Patterns: Elements of Reusable Object-Oriented Software (1994), o Builder pertence à categoria de padrões de criação. É especialmente valioso quando o objeto final possui muitos parâmetros opcionais, quando a ordem de construção importa ou quando a validação precisa ser centralizada antes da entrega do produto.

Problema

Objetos com muitos parâmetros — alguns obrigatórios, outros opcionais, com interdependências — geram dois problemas clássicos quando construídos via construtor ou setters públicos:

  • Telescoping constructor: para cobrir todas as combinações de parâmetros opcionais, você cria múltiplas sobrecargas (new Pedido(items), new Pedido(items, cupom), new Pedido(items, cupom, frete)…). O código que instancia o objeto fica ilegível e propenso a erros de ordem de parâmetros.
  • Objeto em estado inconsistente: com setters públicos, o objeto pode ser usado antes de estar completamente configurado. Validação prematura (no setter individual) não enxerga o estado completo; validação tardia (no método de uso) está longe demais do ponto de criação.

O Builder resolve ambos: cada parâmetro vira um método de passo com nome descritivo, e a validação completa ocorre no build().

Builder vs Abstract Factory

A distinção mais importante — e a que mais confunde:

  • Abstract Factory cria famílias de objetos relacionados de forma instantânea. O foco é na consistência da família de produtos.
  • Builder constrói um único objeto complexo passo a passo. O foco é no processo de montagem e na validação centralizada antes de entregar o produto.

Em resumo: Abstract Factory é sobre o quê criar (qual família); Builder é sobre como construir (qual processo, com quais restrições).

Solução

O Builder organiza o código em quatro participantes:

  1. Builder (interface ou classe base): declara os métodos de configuração de cada passo de construção. Ex.: setUrl(), setMetodo(), addHeader(), build().
  2. ConcreteBuilder: implementa os passos, acumula o estado intermediário e fornece o build() que valida e retorna o produto. Em TypeScript, cada passo retorna this para encadeamento fluente.
  3. Director (opcional): encapsula sequências de construção comuns, chamando os passos do builder numa ordem predefinida. Útil quando as mesmas configurações canônicas aparecem em múltiplos lugares.
  4. Product: o objeto complexo resultante. Frequentemente imutável — após build(), nenhum método de configuração pode mais alterá-lo.

O segredo do padrão está no build(): é ali que a validação centralizada ocorre. Antes de build(), o builder é mutável e tolerante; após, o produto é imutável e garantidamente válido.

Estrutura

Diagrama UML simplificado com o exemplo de builder de requisição HTTP:

          HttpRequest (Product — imutável)
┌──────────────────────────────────────────────────────┐
│ + readonly url: string                               │
│ + readonly metodo: "GET"|"POST"|"PUT"|"PATCH"|"DEL." │
│ + readonly headers: Record<string, string>           │
│ + readonly body?: string                             │
└──────────────────────────────────────────────────────┘
                         ▲
                         │ cria
            HttpRequestBuilder (ConcreteBuilder)
┌──────────────────────────────────────────────────────┐
│ - _url: string                                       │
│ - _metodo: Metodo                                    │
│ - _headers: Record<string, string>                   │
│ - _body?: string                                     │
├──────────────────────────────────────────────────────┤
│ + setUrl(url): this         ← fluent (retorna this)  │
│ + setMetodo(m): this                                 │
│ + addHeader(k, v): this                              │
│ + setBody(b): this                                   │
│ + build(): HttpRequest      ← valida e entrega       │
└──────────────────────────────────────────────────────┘

Fluxo de construção:

  const req = new HttpRequestBuilder()
    .setUrl("https://api.exemplo.com/usuarios")
    .setMetodo("POST")
    .addHeader("Content-Type", "application/json")
    .setBody('{"nome":"Ana"}')
    .build();               // ← validação centralizada ocorre aqui

Exemplos de código

Exemplo 1 — Builder de requisição HTTP com validação no build()

O builder acumula as configurações passo a passo. O método build() valida o estado completo e cria o produto imutável — impedindo que requisições inválidas existam no sistema.

type Metodo = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

// ── Product (imutável) ────────────────────────────────────────
class HttpRequest {
  constructor(
    public readonly url: string,
    public readonly metodo: Metodo,
    public readonly headers: Readonly<Record<string, string>>,
    public readonly body?: string,
  ) {}
}

// ── Builder ───────────────────────────────────────────────────
class HttpRequestBuilder {
  private _url: string = "";
  private _metodo: Metodo = "GET";
  private _headers: Record<string, string> = {};
  private _body?: string;

  setUrl(url: string): this {
    this._url = url;
    return this;
  }

  setMetodo(metodo: Metodo): this {
    this._metodo = metodo;
    return this;
  }

  addHeader(chave: string, valor: string): this {
    this._headers[chave] = valor;
    return this;
  }

  setBody(body: string): this {
    this._body = body;
    return this;
  }

  // Validação centralizada: ocorre uma única vez, antes de criar o produto.
  build(): HttpRequest {
    if (!this._url) {
      throw new Error("HttpRequest: url é obrigatória.");
    }
    if (["POST", "PUT", "PATCH"].includes(this._metodo) && !this._body) {
      throw new Error(`HttpRequest: body é obrigatório para ${this._metodo}.`);
    }
    return new HttpRequest(
      this._url,
      this._metodo,
      { ...this._headers },
      this._body,
    );
  }
}

// ── Uso ──────────────────────────────────────────────────────
const req = new HttpRequestBuilder()
  .setUrl("https://api.exemplo.com/usuarios")
  .setMetodo("POST")
  .addHeader("Content-Type", "application/json")
  .addHeader("Authorization", "Bearer token123")
  .setBody('{"nome":"Ana","email":"ana@exemplo.com"}')
  .build();

console.log(req.metodo); // "POST"
console.log(req.url);    // "https://api.exemplo.com/usuarios"

// Tentativa de build inválido:
try {
  new HttpRequestBuilder()
    .setUrl("https://api.exemplo.com/usuarios")
    .setMetodo("POST")
    .build(); // Falta body!
} catch (e) {
  console.error((e as Error).message);
  // → HttpRequest: body é obrigatório para POST.
}

Exemplo 2 — Director: sequências de construção reutilizáveis

O Director encapsula as sequências canônicas de construção, liberando o código cliente de repetir os mesmos passos em múltiplos lugares. O Director não sabe qual builder concreto está usando — trabalha pela interface, não pela implementação.

// Director: conhece as sequências canônicas de construção.
class ApiClientDirector {
  constructor(private readonly builder: HttpRequestBuilder) {}

  // Constrói uma requisição GET autenticada padrão.
  buildGetAutenticado(url: string, token: string): HttpRequest {
    return this.builder
      .setUrl(url)
      .setMetodo("GET")
      .addHeader("Authorization", `Bearer ${token}`)
      .addHeader("Accept", "application/json")
      .build();
  }

  // Constrói uma requisição POST com JSON autenticada padrão.
  buildPostJson(url: string, token: string, payload: unknown): HttpRequest {
    return this.builder
      .setUrl(url)
      .setMetodo("POST")
      .addHeader("Authorization", `Bearer ${token}`)
      .addHeader("Content-Type", "application/json")
      .addHeader("Accept", "application/json")
      .setBody(JSON.stringify(payload))
      .build();
  }
}

// ── Uso ──────────────────────────────────────────────────────
const director = new ApiClientDirector(new HttpRequestBuilder());

const getReq = director.buildGetAutenticado(
  "https://api.exemplo.com/perfil",
  "abc123",
);
console.log(`${getReq.metodo} ${getReq.url}`);
// → GET https://api.exemplo.com/perfil

const postReq = director.buildPostJson(
  "https://api.exemplo.com/pedidos",
  "abc123",
  { produto: "Caderno", quantidade: 2 },
);
console.log(`${postReq.metodo} ${postReq.url}`);
// → POST https://api.exemplo.com/pedidos

Quando usar

  • Objetos com muitos parâmetros opcionais: quando um construtor começa a ter 4 ou mais parâmetros — especialmente com vários opcionais — o Builder torna o código cliente legível e resistente a erros de ordem.
  • Quando a validação centralizada importa: o build() é o único ponto onde todas as regras são verificadas em conjunto, garantindo que o produto só existe em estado válido.
  • Quando o produto deve ser imutável: o Builder coleta a configuração mutável e entrega um produto imutável — padrão clássico para objetos de valor (value objects) e DTOs.
  • Quando a ordem de construção importa: o Director encapsula a ordem, garantindo que passos obrigatórios nunca sejam esquecidos.

Quando evitar

  • Objetos simples com poucos parâmetros fixos: 2–3 parâmetros obrigatórios sem opcionais não justificam uma classe builder intermediária.
  • Quando não há validação real no build(): se o build() apenas instancia sem validar, o Builder é só açúcar sintático para setters encadeados — avalie se o ganho de legibilidade justifica a classe extra.
  • Quando a imutabilidade do produto não importa: se o objeto precisa ser mutável após a criação de qualquer forma, o padrão perde parte do seu apelo principal.

Prós e contras

Prós

  • Elimina o telescoping constructor: cada parâmetro opcional tem seu próprio método com nome descritivo.
  • Validação centralizada no build(): o produto só existe se for válido — impossível ter objeto em estado inconsistente.
  • Suporta produtos imutáveis: o builder é mutável durante a configuração; o produto entregue é imutável.
  • Legível no site de uso: .setUrl(...).setMetodo("POST").addHeader(...).build() documenta a intenção melhor que uma lista posicional de parâmetros.
  • O Director permite reutilizar sequências de construção comuns sem duplicação.

Contras

  • Mais código: cada produto exige uma classe builder equivalente — overhead para objetos simples.
  • O cliente pode chamar build() esquecendo passos obrigatórios — a validação resolve em runtime, mas não em tempo de compilação.
  • O encadeamento fluente com this em TypeScript pode causar surpresas com subclasses; o tipo de retorno exige cuidado.

Armadilhas comuns

1. Builder sem validação no build() — apenas fluent setters

Atenção: O erro mais comum é criar métodos encadeados que apenas copiam valores para campos, e um build() que instancia sem validar nada. Isso é uma API fluente sobre setters — não é o padrão Builder. A essência do padrão é a validação centralizada e o produto garantidamente válido. Sem isso, você tem complexidade extra sem o benefício principal.

2. Reutilizar o builder após build()

Chamar métodos de configuração após build() e chamar build() novamente pode produzir produtos com estado diferente do esperado, especialmente quando o builder acumula itens (como headers). Duas estratégias comuns: (a) lançar exceção se build() for chamado duas vezes, ou (b) resetar o estado interno após cada build(), tornando o builder reutilizável de forma explícita.

3. Confundir Builder com Abstract Factory

Abstract Factory entrega objetos prontos de uma família em uma única chamada. Builder monta um único objeto complexo passo a passo com validação. Se você precisa garantir que botão e checkbox sejam do mesmo tema, use Abstract Factory. Se você precisa configurar uma requisição HTTP com muitos cabeçalhos opcionais e validar tudo antes de enviá-la, use Builder.

4. Director desnecessário

O Director é opcional. Se não houver sequências de construção reutilizadas em múltiplos lugares, o cliente pode chamar os passos do builder diretamente sem a camada extra. Introduza o Director apenas quando a mesma sequência aparece em três ou mais pontos do código — DRY aplicado à construção.

Padrões relacionados

O Builder interage com outros padrões de criação e estruturais:

O Abstract Factory é frequentemente comparado ao Builder: ambos são padrões de criação, mas Abstract Factory cria famílias de objetos simples de uma vez, enquanto Builder monta um único objeto complexo passo a passo com validação centralizada. O Prototype pode ser usado dentro de um builder quando a criação parte de uma configuração base clonada — o builder clona o protótipo e aplica customizações por cima. O Composite frequentemente é o produto sendo construído pelo Builder: estruturas em árvore (documentos, pipelines, árvores de UI) são candidatas naturais à construção passo a passo.