Коли використовувати 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]] — копіювання об’єктів
- [[3. Що таке Singleton]] — коли Builder потрібен для складного Singleton
- [[1. Що таке патерни проектування]] — загальний вступ
- [[2. Які категорії патернів існують]] — Creational патерни