Строитель (Builder)

Строитель отделяет пошаговую сборку объекта от его использования. Он полезен, когда объект имеет много параметров, часть из них необязательна, а допустимые сочетания нужно проверить до получения результата.

Сигналы применения

  • конструктор трудно читать из-за нескольких аргументов одного типа;
  • появилось много перегруженных конструкторов;
  • объект должен стать неизменяемым сразу после создания;
  • существуют именованные сценарии сборки: «быстрый», «точный», «отладочный»;
  • создание состоит из шагов, но клиенту не стоит знать их порядок.

Builder не нужен для объекта с двумя обязательными полями. В таком случае обычный конструктор или статическая фабрика проще.

Роли и взаимодействие

  • Product — итоговый объект;
  • Builder — хранит промежуточное состояние и предоставляет именованные шаги;
  • Director — необязателен; собирает повторяемые сценарии;
  • Client — выбирает строитель, настраивает его и вызывает build().

build() — граница, на которой проверяются инварианты. До этого момента существует только строитель, а не частично готовый продукт.

Пример на Java

public record BenchmarkPlan(int threads, int seconds, boolean warmup) {
    public BenchmarkPlan {
        if (threads < 1 || seconds < 1) throw new IllegalArgumentException();
    }
    public static Builder builder() { return new Builder(); }
    public static final class Builder {
        private int threads = 1;
        private int seconds = 30;
        private boolean warmup;
        public Builder threads(int value) { threads = value; return this; }
        public Builder durationSeconds(int value) { seconds = value; return this; }
        public Builder withWarmup() { warmup = true; return this; }
        public BenchmarkPlan build() { return new BenchmarkPlan(threads, seconds, warmup); }
    }
}
var plan = BenchmarkPlan.builder()
        .threads(8)
        .durationSeconds(120)
        .withWarmup()
        .build();

В Java строитель часто делают вложенным классом: так он находится рядом с продуктом и может вызывать его закрытый конструктор. Для иерархий наследования Builder заметно усложняется из-за рекурсивных generic-типов; композиция обычно понятнее.

Компромиссы

  • вызов становится читаемым, но число классов и методов растёт;
  • легко задать значения по умолчанию, но важно отличать «не задано» от null;
  • строитель можно переиспользовать, однако это создаёт риск утечки старого состояния;
  • валидация централизована, если она действительно выполняется в build().

Чем отличается

Вопросы на собеседовании

  1. Зачем Builder при наличии именованных параметров? В Java именованных аргументов нет; методы строителя дают имена и скрывают порядок.
  2. Обязателен ли Director? Нет. Он нужен только для повторяемых рецептов сборки.
  3. Где валидировать результат? В build() или в конструкторе продукта, чтобы невозможно было получить объект с нарушенными инвариантами.
  4. Можно ли сделать Builder потокобезопасным? Можно, но обычно не нужно: строитель создают локально и не разделяют между потоками.
  5. Когда Builder избыточен? Когда параметров мало и все они обязательны.

Связи: Порождающие паттерны, Шаблоны проектирования, Принципы SOLID.