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