Pular para o conteúdo

Builder

Separar a construção de um objeto complexo de sua representação, de modo que o mesmo processo de construção possa criar diferentes representações.

Quando um objeto tem muitos parâmetros — vários opcionais — surgem dois anti-padrões clássicos:

  1. Telescoping constructor: uma cascata de construtores sobrecarregados (new Pizza(tamanho), new Pizza(tamanho, queijo), new Pizza(tamanho, queijo, borda)…). Fica ilegível e propenso a erro de ordem de argumentos.
  2. JavaBeans (setters): construir com um construtor vazio e vários set. O objeto passa por estados inconsistentes durante a montagem e não pode ser imutável.

O Builder resolve ambos: a montagem é feita por chamadas nomeadas e encadeadas, e o objeto final pode ser imutável, construído de uma vez no build().

public final class ConexaoHttp {
private final String url;
private final int timeoutMs;
private final int maxRetries;
private final boolean keepAlive;
private final String proxy;
public ConexaoHttp(String url) { this(url, 30000); }
public ConexaoHttp(String url, int timeoutMs) { this(url, timeoutMs, 3); }
public ConexaoHttp(String url, int timeoutMs, int maxRetries) {
this(url, timeoutMs, maxRetries, true);
}
public ConexaoHttp(String url, int timeoutMs, int maxRetries, boolean keepAlive) {
this(url, timeoutMs, maxRetries, keepAlive, null);
}
public ConexaoHttp(String url, int timeoutMs, int maxRetries,
boolean keepAlive, String proxy) {
this.url = url;
this.timeoutMs = timeoutMs;
this.maxRetries = maxRetries;
this.keepAlive = keepAlive;
this.proxy = proxy;
}
}

A chamada new ConexaoHttp("http://api", 5000, 2, true, null) não diz nada sobre o que cada número significa, e quem só quer trocar keepAlive é obrigado a preencher tudo antes.

import java.util.Objects;
public final class ConexaoHttp {
private final String url;
private final int timeoutMs;
private final int maxRetries;
private final boolean keepAlive;
private final String proxy;
private ConexaoHttp(Builder b) {
this.url = b.url;
this.timeoutMs = b.timeoutMs;
this.maxRetries = b.maxRetries;
this.keepAlive = b.keepAlive;
this.proxy = b.proxy;
}
public static Builder para(String url) {
return new Builder(url);
}
public String url() { return url; }
public int timeoutMs() { return timeoutMs; }
public int maxRetries() { return maxRetries; }
public boolean keepAlive() { return keepAlive; }
public String proxy() { return proxy; }
public static final class Builder {
private final String url;
private int timeoutMs = 30_000;
private int maxRetries = 3;
private boolean keepAlive = true;
private String proxy;
private Builder(String url) {
this.url = Objects.requireNonNull(url, "url obrigatória");
}
public Builder timeoutMs(int valor) {
if (valor <= 0) throw new IllegalArgumentException("timeout deve ser positivo");
this.timeoutMs = valor;
return this;
}
public Builder maxRetries(int valor) {
if (valor < 0) throw new IllegalArgumentException("retries não pode ser negativo");
this.maxRetries = valor;
return this;
}
public Builder keepAlive(boolean valor) {
this.keepAlive = valor;
return this;
}
public Builder proxy(String valor) {
this.proxy = valor;
return this;
}
public ConexaoHttp build() {
return new ConexaoHttp(this);
}
}
}

Uso:

ConexaoHttp conexao = ConexaoHttp.para("https://api.exemplo.com")
.timeoutMs(5_000)
.maxRetries(2)
.proxy("http://proxy.interno:8080")
.build();

Cada passo é nomeado, a ordem é livre, defaults ficam explícitos no builder, e as validações ficam no builder — distribuídas nos setters e/ou centralizadas no build() — antes de o objeto imutável nascer.

classDiagram
    class ConexaoHttp {
        -url : String
        -timeoutMs : int
        -maxRetries : int
        +para(String) Builder
    }
    class Builder {
        -url : String
        -timeoutMs : int
        +timeoutMs(int) Builder
        +maxRetries(int) Builder
        +build() ConexaoHttp
    }
    class Cliente
    ConexaoHttp ..> Builder : cria via factory estática
    Builder ..> ConexaoHttp : constrói
    Cliente --> Builder : encadeia passos
  • A favor: legibilidade nas chamadas, suporte a imutabilidade, validação centralizada, defaults explícitos, montagem incremental.
  • Contra: mais código boilerplate (uma classe interna extra), duplicação de campos entre builder e produto. Para objetos simples, o custo não compensa.

Java record gera construtor canônico, acessores, equals, hashCode e toString automaticamente. Prefira record quando:

  • Todos os campos são obrigatórios (ou há poucos opcionais).
  • Não há montagem em etapas nem múltiplas representações.
  • O objeto é essencialmente um agrupamento imutável de dados — o caso típico de um Value Object.
public record Dinheiro(long centavos, String moeda) {
public Dinheiro {
Objects.requireNonNull(moeda);
if (centavos < 0) throw new IllegalArgumentException("negativo");
}
}

Use Builder apenas quando o número de parâmetros opcionais, a validação composta ou a montagem passo a passo justificarem. Muitos Value Objects dispensam Builder completamente — recorrer a ele por reflexo é over-engineering.

  • Builder mutável reutilizado como se fosse o objeto: manter o builder vivo e chamar build() várias vezes esperando cópias independentes pode compartilhar referências mutáveis.
  • build() sem validação: se as invariantes não são checadas no build(), o Builder só troca o telescoping constructor por outra via para criar objetos inválidos.
  • Builder para 2 campos obrigatórios: over-engineering; um construtor ou record basta.
  • Setters que retornam void: quebram a fluência; um builder deve retornar this (ou um tipo de estágio) a cada passo.

1. Quais dois anti-padrões o Builder substitui, e como?

Resposta

O telescoping constructor (cascata de construtores sobrecarregados, ilegível) e a construção via JavaBeans/setters (objeto passa por estados inconsistentes, não pode ser imutável). O Builder oferece chamadas nomeadas e encadeadas com montagem incremental e objeto final imutável.

2. Onde devem morar as validações de invariante num Builder?

Resposta

Idealmente distribuídas nos setters (falha rápida por campo) e/ou centralizadas no build(), antes de instanciar o objeto imutável. Sem isso, o Builder apenas troca um caminho para criar objetos inválidos por outro.

3. Quando um record é preferível a um Builder?

Resposta

Quando os campos são majoritariamente obrigatórios, não há montagem em etapas nem múltiplas representações, e o objeto é um agrupamento imutável de dados — o caso típico de Value Objects. O record gera construtor, acessores e equals/hashCode sem boilerplate.

4. Por que aplicar Builder a um objeto de dois campos obrigatórios é problemático?

Resposta

É over-engineering: adiciona uma classe interna e duplicação de campos sem resolver nenhum problema real (não há opcionais nem montagem incremental). Um construtor simples ou record é mais claro e barato.