Строитель (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().
Чем отличается
- от Фабричный метод Factory Method — фабричный метод выбирает тип продукта, а Builder управляет множеством параметров и этапов;
- от Абстрактная фабрика Abstract Factory — фабрика создаёт согласованное семейство объектов, Builder собирает один сложный объект;
- от Прототип Prototype — Prototype начинает с готового экземпляра, Builder — с описания будущего экземпляра.
Вопросы на собеседовании
- Зачем Builder при наличии именованных параметров? В Java именованных аргументов нет; методы строителя дают имена и скрывают порядок.
- Обязателен ли Director? Нет. Он нужен только для повторяемых рецептов сборки.
- Где валидировать результат? В
build()или в конструкторе продукта, чтобы невозможно было получить объект с нарушенными инвариантами. - Можно ли сделать Builder потокобезопасным? Можно, но обычно не нужно: строитель создают локально и не разделяют между потоками.
- Когда Builder избыточен? Когда параметров мало и все они обязательны.
Связи: Порождающие паттерны, Шаблоны проектирования, Принципы SOLID.