Pular para o conteúdo

Builder é um padrão de projeto criacional que ajuda a construir objetos complexos passo a passo, deixando o código:

  • mais legível,
  • mais flexível para criar variações,
  • e menos dependente de construtores gigantes ou de muitos parâmetros opcionais.

Em vez de ter um construtor enorme ou vários construtores sobrecarregados, o Builder permite montar o objeto “em partes”, de forma fluente.


Imagine que você precisa criar um objeto com muitos atributos, vários deles opcionais ou que têm combinações diferentes:

  • Um cadastro de aluno com dados pessoais, endereços, contatos, preferências, permissões, etc.
  • Um relatório com dezenas de configurações (formato, filtros, ordenação, colunas, estilos…).
  • Um “pedido” em um sistema de e-commerce com vários itens, descontos, frete, cupom, endereço de entrega, etc.

Sem Builder, aparecem alguns problemas comuns:

  1. Construtor telescópico (mil parâmetros)

    Pedido pedido = new Pedido(
    cliente,
    enderecoEntrega,
    itens,
    desconto,
    cupom,
    frete,
    observacoes,
    /* ...e por aí vai... */
    );
    • Difícil de ler.
    • Fácil passar parâmetros na ordem errada.
    • Vira dor de cabeça quando novos campos aparecem.
  2. Muitos setters soltos

    Pedido pedido = new Pedido();
    pedido.setCliente(cliente);
    pedido.setEnderecoEntrega(enderecoEntrega);
    pedido.setItens(itens);
    pedido.setDesconto(desconto);
    // ... vários setters ...
    • O objeto pode ficar em um estado inconsistente no meio da montagem.
    • Fica difícil garantir que tudo necessário foi configurado.
  3. Várias combinações de construção

    • Às vezes você quer um “pedido simples”.
    • Às vezes um “pedido promocional”.
    • Às vezes um “pedido corporativo”.

    Sem um padrão claro, esse conhecimento de “como montar” se espalha pelo código.

Precisamos de uma forma de organizar a construção de objetos complexos, tornando o processo:

  • passo a passo,
  • legível,
  • reutilizável,
  • e com variações controladas.

O padrão Builder propõe separar o “como construir” do “que está sendo construído”.

As ideias principais são:

  1. Produto (Product)

    • É o objeto complexo que queremos montar.
    • Ex.: Sanduiche, Pedido, Relatorio.
  2. Builder (Interface ou Classe Abstrata)

    • Define os passos para montar o produto.
    • Ex.: adicionarPao(), adicionarProteina(), adicionarMolho().
  3. Concrete Builders (Builders Concretos)

    • Implementam esses passos para montar variações diferentes do produto.
    • Ex.: SanduicheFrangoBuilder, SanduicheVeganoBuilder.
  4. Director (Opcional)

    • Classe que orquestra a construção, chamando os passos do Builder numa certa ordem.
    • Ex.: Cozinha que sabe como montar “combo simples” ou “combo completo”.

Com isso:

  • O código que usa o Builder não precisa saber como o objeto é montado em detalhes.
  • É fácil criar diferentes versões do mesmo objeto mudando apenas o Builder concreto.
  • A lógica de construção fica centralizada e organizada.

Pense em uma lanchonete universitária que faz sanduíches:

  • O sanduíche pronto é o Produto.
  • A pessoa que monta o sanduíche (cozinheiro) é o Builder.
  • A cozinha / sistema de pedidos, que diz “faça um X-Salada” ou “faça um Vegano”, é o Director.

Passos típicos:

  1. Escolher o tipo de pão.
  2. Escolher a proteína (carne, frango, falafel, nenhum…).
  3. Adicionar queijo ou não.
  4. Adicionar vegetais (alface, tomate, cebola…).
  5. Adicionar molhos (maionese, mostarda, barbecue…).

Cada tipo de sanduíche segue a mesma sequência de passos, mas com ingredientes diferentes:

  • X-Salada: pão normal, carne, queijo, alface, tomate, maionese.
  • Vegano: pão integral, hambúrguer de grão-de-bico, alface, tomate, cebola, molho especial sem leite.

O Builder funciona assim:

  • O “cozinheiro” sabe como executar cada passo (botar pão, pôr proteína, etc.).
  • A “cozinha” (Director) sabe quais passos chamar para cada tipo de sanduíche.
  • No final, você chama algo como builder.getResultado() e recebe o sanduíche montado.

Use Builder quando:

  • Um objeto tiver muitos parâmetros, especialmente opcionais ou com combinações diferentes.
  • Você quiser evitar construtores gigantes e difíceis de ler.
  • Você precisar construir diferentes representações do mesmo tipo de objeto.
  • O processo de construção precisar ser reutilizável e centralizado.

Exemplos em Engenharia de Software:

  • Montagem de requisições HTTP complexas (headers, body, autenticação, cache…).
  • Criação de objetos de configuração (config do sistema, config de build, config de ambiente).
  • Geração de relatórios (HTML, PDF, CSV) usando os mesmos dados, mas mudando a forma de montar.

Em pseudocódigo orientado a objetos:

// Produto
class Sanduiche {
// ...atributos: tipoPao, proteina, queijo, vegetais, molhos, etc...
}
// Builder
interface SanduicheBuilder {
void reset();
void adicionarPao();
void adicionarProteina();
void adicionarQueijo();
void adicionarVegetais();
void adicionarMolhos();
Sanduiche getResultado();
}
// Builders concretos
class SanduicheFrangoBuilder implements SanduicheBuilder {
// ...implementa os passos montando um sanduíche de frango...
}
class SanduicheVeganoBuilder implements SanduicheBuilder {
// ...implementa os passos montando um sanduíche vegano...
}
// Director
class Cozinha {
public Sanduiche prepararSanduicheFrango(SanduicheBuilder builder) {
builder.reset();
builder.adicionarPao();
builder.adicionarProteina();
builder.adicionarQueijo();
builder.adicionarVegetais();
builder.adicionarMolhos();
return builder.getResultado();
}
// ...outros métodos para diferentes "receitas"...
}

public class Sanduiche {
private String tipoPao;
private String proteina;
private boolean temQueijo;
private String vegetais;
private String molhos;
// Construtor padrão ou pacote
Sanduiche() {}
// setters usados apenas pelo Builder
void setTipoPao(String tipoPao) { this.tipoPao = tipoPao; }
void setProteina(String proteina) { this.proteina = proteina; }
void setTemQueijo(boolean temQueijo) { this.temQueijo = temQueijo; }
void setVegetais(String vegetais) { this.vegetais = vegetais; }
void setMolhos(String molhos) { this.molhos = molhos; }
@Override
public String toString() {
return "Sanduiche{" +
"tipoPao='" + tipoPao + '\'' +
", proteina='" + proteina + '\'' +
", temQueijo=" + temQueijo +
", vegetais='" + vegetais + '\'' +
", molhos='" + molhos + '\'' +
'}';
}
}
public interface SanduicheBuilder {
void reset();
void adicionarPao();
void adicionarProteina();
void adicionarQueijo();
void adicionarVegetais();
void adicionarMolhos();
Sanduiche getResultado();
}
public class SanduicheFrangoBuilder implements SanduicheBuilder {
private Sanduiche sanduiche;
@Override
public void reset() {
sanduiche = new Sanduiche();
}
@Override
public void adicionarPao() {
sanduiche.setTipoPao("Pão francês");
}
@Override
public void adicionarProteina() {
sanduiche.setProteina("Frango grelhado");
}
@Override
public void adicionarQueijo() {
sanduiche.setTemQueijo(true);
}
@Override
public void adicionarVegetais() {
sanduiche.setVegetais("Alface, tomate");
}
@Override
public void adicionarMolhos() {
sanduiche.setMolhos("Maionese");
}
@Override
public Sanduiche getResultado() {
return sanduiche;
}
}
public class SanduicheVeganoBuilder implements SanduicheBuilder {
private Sanduiche sanduiche;
@Override
public void reset() {
sanduiche = new Sanduiche();
}
@Override
public void adicionarPao() {
sanduiche.setTipoPao("Pão integral");
}
@Override
public void adicionarProteina() {
sanduiche.setProteina("Hambúrguer de grão-de-bico");
}
@Override
public void adicionarQueijo() {
sanduiche.setTemQueijo(false); // vegano, sem queijo
}
@Override
public void adicionarVegetais() {
sanduiche.setVegetais("Alface, tomate, cebola roxa");
}
@Override
public void adicionarMolhos() {
sanduiche.setMolhos("Molho especial vegano");
}
@Override
public Sanduiche getResultado() {
return sanduiche;
}
}
public class Cozinha {
public Sanduiche prepararSanduicheCompleto(SanduicheBuilder builder) {
builder.reset();
builder.adicionarPao();
builder.adicionarProteina();
builder.adicionarQueijo();
builder.adicionarVegetais();
builder.adicionarMolhos();
return builder.getResultado();
}
public Sanduiche prepararSanduicheSimples(SanduicheBuilder builder) {
builder.reset();
builder.adicionarPao();
builder.adicionarProteina();
builder.adicionarMolhos();
return builder.getResultado();
}
}
public class AppLanchonete {
public static void main(String[] args) {
Cozinha cozinha = new Cozinha();
SanduicheBuilder builderFrango = new SanduicheFrangoBuilder();
SanduicheBuilder builderVegano = new SanduicheVeganoBuilder();
Sanduiche frangoCompleto = cozinha.prepararSanduicheCompleto(builderFrango);
Sanduiche veganoSimples = cozinha.prepararSanduicheSimples(builderVegano);
System.out.println(frangoCompleto);
System.out.println(veganoSimples);
}
}

Perceba:

  • O código cliente não sabe detalhes de como cada sanduíche é montado.
  • Para adicionar um novo tipo de sanduíche, basta criar um novo Builder concreto.

  • Código de construção mais legível

    • Evita construtores com muitos parâmetros.
    • Facilita entender “passo a passo” o que está sendo configurado.
  • Suporte fácil a variações

    • Builders diferentes montam versões diferentes do mesmo tipo de objeto.
    • A lógica de cada variação fica isolada na sua própria classe.
  • Objetos sempre em estado consistente

    • Você pode garantir que certas etapas sempre acontecem antes de expor o objeto pronto.
  • Reutilização do processo de construção

    • O Director pode ter “receitas” prontas de construção (como combos de sanduíche).

  • Mais classes

    • Introduz mais código estrutural (Produto, Builder, Builders concretos, Director).
    • Pode parecer exagero para objetos simples.
  • Complexidade inicial maior

    • Exige conhecer a estrutura do padrão.
    • Nem sempre vale a pena para casos triviais.

  • Factory Method / Abstract Factory

    • Fábricas focam em criar objetos de uma vez.
    • Builder foca em construir passo a passo, em especial objetos complexos.
  • Prototype

    • Prototype copia um objeto já existente.
    • Builder monta um objeto novo desde o início, passo a passo.
  • Composite

    • Builder pode ser usado para montar estruturas complexas de objetos (árvores, hierarquias), muitas vezes combinando com Composite.
  • Fluent Interface

    • Builders muitas vezes expõem uma interface fluente, permitindo encadear chamadas (.setX().setY().build()), melhorando a legibilidade.

Variação comum: Builder “Fluente” simplificado

Seção intitulada “Variação comum: Builder “Fluente” simplificado”

Uma forma muito comum de usar Builder em Java moderno (sem Director explícito) é ter um Builder interno na própria classe:

public class Pedido {
private String cliente;
private String enderecoEntrega;
private double desconto;
private Pedido() {}
public static class Builder {
private String cliente;
private String enderecoEntrega;
private double desconto;
public Builder cliente(String cliente) {
this.cliente = cliente;
return this;
}
public Builder enderecoEntrega(String enderecoEntrega) {
this.enderecoEntrega = enderecoEntrega;
return this;
}
public Builder desconto(double desconto) {
this.desconto = desconto;
return this;
}
public Pedido build() {
Pedido p = new Pedido();
p.cliente = this.cliente;
p.enderecoEntrega = this.enderecoEntrega;
p.desconto = this.desconto;
return p;
}
}
}

Uso:

Pedido pedido = new Pedido.Builder()
.cliente("Aluno Dev")
.enderecoEntrega("Campus Central")
.desconto(0.1)
.build();

Essa abordagem é muito usada em APIs Java e frameworks modernos, e segue a mesma ideia central do padrão Builder: construção de objetos complexos de forma clara e flexível.