Вопрос 8 · Раздел 2

Когда использовать Builder?

Гарантирует заполнение обязательных полей на уровне компиляции:

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Builder — паттерн для пошагового создания сложных объектов.

Пошагового = вы вызываете по одному методу на каждое поле, в любом порядке, и только build() создаёт финальный объект. Объект никогда не существует в частично заполненном состоянии.

Проблема: Когда у объекта много параметров, конструктор становится неудобным:

// ❌ "Телескопический" конструктор — непонятно, что есть что
User user = new User("Ivan", "Ivanov", "ivan@mail.com", 
                     "+79001234567", "Moscow", 25, true, false);

Решение — Builder:

// ✅ Понятно, что есть что
User user = User.builder()
    .firstName("Ivan")
    .lastName("Ivanov")
    .email("ivan@mail.com")
    .phone("+79001234567")
    .city("Moscow")
    .age(25)
    .active(true)
    .build();

Когда использовать:

  • Больше 3-4 параметров в конструкторе
  • Есть необязательные параметры
  • Нужно сделать код читаемым

Когда НЕ использовать Builder

  1. Объекты с 1-2 параметрами — конструктор читаемее
  2. Value-объекты (Point(x,y)) — конструктор или static factory method
  3. Hot-path (миллионы созданий/сек) — overhead на builder-объект

Builder vs Factory Method

Builder — когда много параметров (4+), особенно опциональных. Factory Method — когда есть несколько способов создать объект (fromJSON, fromXML, fromCSV). Constructor — когда 1-2 обязательных параметра.


🟡 Middle Level

Проблемы, которые решает Builder

1. Телескопические конструкторы:

// ❌ 4 конструктора для всех комбинаций
public User(String name) { ... }
public User(String name, String email) { ... }
public User(String name, String email, int age) { ... }
public User(String name, String email, int age, String city) { ... }

// ✅ Builder — любая комбинация
User.builder().name("Ivan").city("Moscow").build();

2. JavaBeans (сеттеры) — небезопасно:

// ❌ Объект может быть в невалидном состоянии
User user = new User();
user.setName("Ivan");
user.setEmail("ivan@mail.com");
// ... промежуточное состояние видно другим потокам!
user.setAge(25);
user.validate();  // Забыли вызвать?

3. Builder гарантирует, что объект никогда не будет в частично заполненном состоянии:

// ✅ Все поля установлены до того, как объект станет доступен
User user = User.builder()
    .name("Ivan")
    .email("ivan@mail.com")
    .age(25)
    .build();  // Валидация внутри build()
// Объект либо создан целиком, либо exception

Реализация Builder

public class User {
    private final String name;     // final — иммутабельность!
    private final String email;
    private final int age;
    private final String city;     // Опциональное поле
    
    private User(Builder builder) {
        this.name = builder.name;
        this.email = builder.email;
        this.age = builder.age;
        this.city = builder.city;
    }
    
    public static Builder builder() {
        return new Builder();
    }
    
    public static class Builder {
        private String name;
        private String email;
        private int age;
        private String city;
        
        public Builder name(String name) { 
            this.name = name; 
            return this;  // Fluent API
        }
        
        public Builder email(String email) { 
            this.email = email; 
            return this; 
        }
        
        public Builder age(int age) { 
            this.age = age; 
            return this; 
        }
        
        public Builder city(String city) { 
            this.city = city; 
            return this; 
        }
        
        public User build() {
            // Валидация
            if (name == null || name.isEmpty()) {
                throw new IllegalStateException("Name is required");
            }
            if (email == null || !email.contains("@")) {
                throw new IllegalStateException("Valid email required");
            }
            return new User(this);
        }
    }
}

Lombok Builder

// Вместо 100 строк кода:
@Builder
public class User {
    private final String name;
    private final String email;
    private final int age;
    private final String city;
}

// Использование
User user = User.builder()
    .name("Ivan")
    .email("ivan@mail.com")
    .build();

Типичные ошибки

  1. Builder для простого объекта
    // ❌ Overengineering
    @Builder
    public class Point {
        private int x;
        private int y;
    }
       
    // ✅ Достаточно конструктора
    public class Point {
        public Point(int x, int y) { this.x = x; this.y = y; }
    }
    
  2. Мутабельный объект после build
    // ❌ Builder создал, но поля не final
    public class User {
        private String name;  // Можно изменить!
    }
       
    // ✅ final поля
    public class User {
        private final String name;  // Нельзя изменить
    }
    

🔴 Senior Level

Wither — метод в функциональном программировании, возвращающий новый объект с изменённым полем (name = name.withX(x)). GC pressure — частые аллокации вызывают частые GC-циклы. Step Builder (Staged Builder) — компилятор не даёт пропустить шаг через интерфейсные типы.

Step Builder (Staged Builder)

Гарантирует заполнение обязательных полей на уровне компиляции:

public class User {
    private final String name;    // Обязательное
    private final String email;   // Обязательное
    private final String city;    // Опциональное
    
    private User(String name, String email, String city) {
        this.name = name;
        this.email = email;
        this.city = city;
    }
    
    // Интерфейсы для каждого шага
    public interface NameStep { EmailStep name(String name); }
    public interface EmailStep { CityStep email(String email); }
    public interface CityStep { 
        CityStep city(String city); 
        User build(); 
    }
    
    // Реализация
    public static NameStep builder() {
        return new UserBuilder();
    }
    
    private static class UserBuilder implements NameStep, EmailStep, CityStep {
        private String name;
        private String email;
        private String city;
        
        public EmailStep name(String name) { 
            this.name = name; 
            return this; 
        }
        
        public CityStep email(String email) { 
            this.email = email; 
            return this; 
        }
        
        public CityStep city(String city) { 
            this.city = city; 
            return this; 
        }
        
        public User build() {
            return new User(name, email, city);
        }
    }
}

// Использование — компилятор не даст пропустить шаги!
User user = User.builder()
    .name("Ivan")     // Обязательно
    .email("i@m.com") // Обязательно
    .city("Moscow")   // Опционально
    .build();

// ❌ Не скомпилируется:
User.builder().name("Ivan").build();  // Ошибка: нет email()!

Copy Builder (toBuilder)

@Builder(toBuilder = true)
public class User {
    private final String name;
    private final String email;
    private final int age;
}

// Создание копии с изменением одного поля
User updated = user.toBuilder()
    .email("new@email.com")
    .build();

// Оригинальный user не изменён!
// → Идеально для иммутабельных структур

Валидация в Builder

@Builder
public class Order {
    private final LocalDate startDate;
    private final LocalDate endDate;
    private final BigDecimal amount;
    
    @Builder
    private Order(LocalDate startDate, LocalDate endDate, BigDecimal amount) {
        // Кросс-полевая валидация
        if (startDate != null && endDate != null && startDate.isAfter(endDate)) {
            throw new IllegalArgumentException("startDate must be before endDate");
        }
        if (amount != null && amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("amount cannot be negative");
        }
        
        this.startDate = startDate;
        this.endDate = endDate;
        this.amount = amount;
    }
}

Performance: Memory Overhead

// Каждый Builder = дополнительный объект в Heap
User user = User.builder()  // → new Builder()
    .name("Ivan")
    .build();

// В Hot-path (1M объектов/сек):
// → 1M Builder объектов → GC pressure

// Бенчмарк:
// Обычный конструктор: 10ns
// Builder: 15ns (+50% overhead)

// Для критичных путей — используйте конструкторы!

Builder с наследованием

// Проблема: Builder родительского класса возвращает Builder, а не подкласс
@Builder
public class User {
    private String name;
}

@Builder
public class Admin extends User {  // ❌ Lombok не поддерживает
    private String role;
}

// Решение: @SuperBuilder
@SuperBuilder
public class User {
    private String name;
}

@SuperBuilder
public class Admin extends User {  // ✅ Работает!
    private String role;
}

Admin admin = Admin.builder()
    .name("Ivan")  // Из родителя
    .role("ADMIN") // Из подкласса
    .build();

Records + Builder

// Records не имеют Builder по умолчанию
public record User(String name, String email, int age) {}

// Ручной Builder для Records
public class User {
    public static Builder builder() { return new Builder(); }
    
    public static class Builder {
        private String name;
        private String email;
        private int age;
        
        public Builder name(String name) { this.name = name; return this; }
        public Builder email(String email) { this.email = email; return this; }
        public Builder age(int age) { this.age = age; return this; }
        
        public User build() { return new User(name, email, age); }
    }
}

// Или использовать Records с Lombok @Builder
@Builder
public record User(String name, String email, int age) {}

Production Experience

Реальный сценарий #1: Builder спас от багов

  • DTO с 15 полями, 10 опциональных
  • 2^10 = 1024 комбинации конструкторов
  • Решение: Builder → любая комбинация
  • Результат: -80% кода, +читаемость

Реальный сценарий #2: Step Builder предотвратил ошибки

  • API клиент: 5 обязательных параметров
  • Разработчики часто забывали заполнить
  • Решение: Step Builder
  • Результат: compile-time гарантия, 0 ошибок

Best Practices

  1. Builder для >4 параметров или опциональных полей
  2. final поля — делайте объект иммутабельным
  3. Валидация в build() — последний рубеж
  4. Step Builder для критичных обязательных полей
  5. toBuilder() для иммутабельных структур
  6. Lombok @Builder для экономии кода
  7. Избегайте в Hot-path (GC pressure)
  8. @SuperBuilder для наследования

Резюме для Senior

  • Builder = атомарность создания + иммутабельность
  • Step Builder = compile-time гарантия обязательных полей
  • toBuilder = Wither-логика для иммутабельных объектов
  • Memory Overhead: +50% на создание Builder объекта
  • Hot-path: избегайте Builder — используйте конструкторы
  • Валидация: кросс-полевая в build(), пошаговая в методах
  • Records: ручной Builder или Lombok @Builder
  • Наследование: @SuperBuilder решает проблему типов

🎯 Шпаргалка для интервью

Обязательно знать:

  • Builder решает проблему телескопических конструкторов (4+ параметров) и небезопасных JavaBeans (сеттеры)
  • Builder гарантирует атомарность: объект никогда не в частично заполненном состоянии
  • Step Builder (Staged Builder) — compile-time гарантия заполнения обязательных полей через интерфейсные типы
  • toBuilder() — Wither-логика: создание копии с изменением полей для иммутабельных объектов
  • Memory overhead: +50% на создание Builder объекта, избегать в Hot-path (1M+ созданий/сек)
  • Lombok @Builder экономит ~100 строк кода, @SuperBuilder для наследования
  • Поля должны быть final — Builder без иммутабельности теряет смысл

Частые уточняющие вопросы:

  • Когда НЕ использовать Builder? — 1-2 параметра (конструктор читаемее), value-объекты (Point), Hot-path (GC pressure)
  • Чем Builder отличается от Factory Method? — Builder для множества параметров (4+), Factory Method для разных способов создания
  • Что такое Step Builder? — Паттерн, где компилятор не даёт пропустить обязательные шаги через интерфейсные типы
  • Почему поля должны быть final? — Иммутабельность: после build() объект нельзя изменить, иначе смысл Builder теряется

Красные флаги (НЕ говорить):

  • “Я использую Builder для объектов с 1-2 параметрами” — overengineering, конструктор читаемее
  • “Builder не нужен с Lombok” — Lombok генерирует тот же Builder, принцип остаётся
  • “Поля могут быть mutable после build()” — тогда Builder не решает проблему безопасности
  • “Step Builder — это то же самое, что обычный Builder” — Step Builder даёт compile-time гарантии

Связанные темы:

  • [[7. В чём разница между Factory Method и Abstract Factory]] — порождающие паттерны
  • [[9. Что такое Prototype pattern]] — копирование объектов
  • [[3. Что такое Singleton]] — когда Builder нужен для сложного Singleton
  • [[1. Что такое паттерны проектирования]] — общее введение
  • [[2. Какие категории паттернов существуют]] — Creational паттерны