Когда использовать Builder?
Гарантирует заполнение обязательных полей на уровне компиляции:
🟢 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-2 параметрами — конструктор читаемее
- Value-объекты (Point(x,y)) — конструктор или static factory method
- 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();
Типичные ошибки
- 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; } } - Мутабельный объект после 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
- Builder для >4 параметров или опциональных полей
- final поля — делайте объект иммутабельным
- Валидация в build() — последний рубеж
- Step Builder для критичных обязательных полей
- toBuilder() для иммутабельных структур
- Lombok @Builder для экономии кода
- Избегайте в Hot-path (GC pressure)
- @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 паттерны