← Todos os padrões

Builder

Creational · ver código-fonte no GitHub

Leia em: English · Português · Español

Category: Creational

O problema

Alguns objetos têm muitos campos, a maioria opcionais, e só alguns obrigatórios. Um construtor que recebe todos eles fica ilegível no ponto de chamada (new Computer("Ryzen 9", 32, 1024, true, false, "extended-warranty") — qual booleano era qual?), e um que ganha uma sobrecarga nova pra cada combinação de campos opcionais (o "telescoping constructor") multiplica combinatoriamente conforme mais opções são adicionadas. Setters em vez de construtor resolvem a legibilidade, mas deixam o objeto mutável e possivelmente incompleto se quem chama esquecer um campo obrigatório.

A solução

Mover a construção pra um objeto separado que acumula valores campo a campo por uma API fluente e encadeável, e só produz o objeto real (imutável) na chamada final de build().

classDiagram
    class Product {
        <<immutable>>
    }
    class Builder {
        +withOptionA(value) Builder
        +withOptionB(value) Builder
        +build() Product
    }
    Builder ..> Product : creates

Exemplo clássico

classic/Computer é o builder fluente clássico dos livros: um cpu obrigatório, três campos opcionais com valores padrão sensatos (ramGb, storageGb, hasGraphicsCard), e um construtor privado, de modo que a única forma de obter um Computer é através de Computer.builder(cpu)....build(). ComputerTest verifica que os padrões se aplicam quando nada mais é definido, que sobrescrever um campo não afeta os outros, e que um campo obrigatório nulo falha rápido com um NPE em vez de produzir um objeto incompleto.

Exemplo aplicado: montagem de proposta de financiamento de veículo

applied/AutoLoanProposal é a mesma estrutura aplicada a uma proposta de financiamento de veículo, do tipo montada no ponto de venda de um banco: dois campos obrigatórios (solicitante, preço do veículo) e quatro adicionais opcionais e independentes (número de parcelas, seguro, um veículo usado como garantia, uma taxa promocional) que não se aplicam a todo negócio. Um construtor comum aqui forçaria cada ponto de chamada a passar false, false, null, false pros negócios que dispensam todo adicional — o builder deixa cada ponto de chamada ler exatamente o que solicita, nada mais. AutoLoanProposalTest cobre o prazo padrão, todos os adicionais combinados, e as duas falhas de validação (preço não positivo, número de parcelas não positivo).

Quando não usar

Cobertura de testes

100% de cobertura de instrução, 100% de cobertura de branch (JaCoCo). Reproduza você mesmo:

./gradlew :creational:builder:jacocoTestReport

Relatório em creational/builder/build/reports/jacoco/test/html/index.html.

Leitura complementar

Testes unitários

src/test/java/com/designpatterns/creational/builder/classic/ComputerTest.java
package com.designpatterns.creational.builder.classic;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class ComputerTest {

    @Test
    void appliesSensibleDefaultsWhenOnlyTheRequiredFieldIsSet() {
        Computer computer = Computer.builder("Ryzen 7").build();

        assertThat(computer.cpu()).isEqualTo("Ryzen 7");
        assertThat(computer.ramGb()).isEqualTo(8);
        assertThat(computer.storageGb()).isEqualTo(256);
        assertThat(computer.hasGraphicsCard()).isFalse();
    }

    @Test
    void overridesOnlyTheFieldsExplicitlySet() {
        Computer computer = Computer.builder("Ryzen 9")
                .ramGb(32)
                .storageGb(1024)
                .withGraphicsCard()
                .build();

        assertThat(computer.ramGb()).isEqualTo(32);
        assertThat(computer.storageGb()).isEqualTo(1024);
        assertThat(computer.hasGraphicsCard()).isTrue();
    }

    @Test
    void rejectsANullCpu() {
        assertThatThrownBy(() -> Computer.builder(null)).isInstanceOf(NullPointerException.class);
    }
}
src/test/java/com/designpatterns/creational/builder/applied/AutoLoanProposalTest.java
package com.designpatterns.creational.builder.applied;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class AutoLoanProposalTest {

    @Test
    void appliesTheDefaultTermWhenNoAddOnsAreRequested() {
        AutoLoanProposal proposal = AutoLoanProposal.builder("applicant-1", 80_000_00L).build();

        assertThat(proposal.applicantId()).isEqualTo("applicant-1");
        assertThat(proposal.vehiclePriceCents()).isEqualTo(80_000_00L);
        assertThat(proposal.installments()).isEqualTo(48);
        assertThat(proposal.insuranceIncluded()).isFalse();
        assertThat(proposal.hasCollateral()).isFalse();
        assertThat(proposal.promotionalRate()).isFalse();
    }

    @Test
    void combinesOnlyTheAddOnsExplicitlyRequested() {
        AutoLoanProposal proposal = AutoLoanProposal.builder("applicant-2", 120_000_00L)
                .installments(60)
                .withInsurance()
                .withCollateral("ABC1D23")
                .promotionalRate()
                .build();

        assertThat(proposal.installments()).isEqualTo(60);
        assertThat(proposal.insuranceIncluded()).isTrue();
        assertThat(proposal.hasCollateral()).isTrue();
        assertThat(proposal.collateralVehiclePlate()).isEqualTo("ABC1D23");
        assertThat(proposal.promotionalRate()).isTrue();
    }

    @Test
    void rejectsANonPositiveVehiclePrice() {
        assertThatThrownBy(() -> AutoLoanProposal.builder("applicant-3", 0L))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void rejectsANonPositiveInstallmentCount() {
        AutoLoanProposal.Builder builder = AutoLoanProposal.builder("applicant-4", 50_000_00L);

        assertThatThrownBy(() -> builder.installments(0)).isInstanceOf(IllegalArgumentException.class);
    }
}

Ver relatório completo de cobertura JaCoCo →