Builder
Intenção (GoF)
Seção intitulada “Intenção (GoF)”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.
Problema que resolve
Seção intitulada “Problema que resolve”Quando um objeto tem muitos parâmetros — vários opcionais — surgem dois anti-padrões clássicos:
- 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. - 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().
O problema, em código
Seção intitulada “O problema, em código”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.
Solução com o padrão
Seção intitulada “Solução com o padrão”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.
Estrutura
Seção intitulada “Estrutura”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
Consequências e trade-offs
Seção intitulada “Consequências e trade-offs”- 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.
Quando preferir um record / value object
Seção intitulada “Quando preferir um record / value object”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.
Anti-padrões e erros comuns
Seção intitulada “Anti-padrões e erros comuns”- 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 nobuild(), 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
recordbasta. - Setters que retornam
void: quebram a fluência; um builder deve retornarthis(ou um tipo de estágio) a cada passo.
Relações
Seção intitulada “Relações”- Categoria: Padrões Criacionais.
- Índice: Design Patterns (GoF).
- Alternativa frequente: Value Objects (via
record). - Padrão criacional relacionado para famílias de objetos: Abstract Factory.
Perguntas de revisão
Seção intitulada “Perguntas de revisão”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.