Строитель (Builder)
Собирает сложный объект по шагам: обязательные части, необязательные в любом порядке, одна проверка в конце.
Проблема
Письмо состоит из адресата и темы (обязательно), текста, копий и вложений (по желанию). Конструктор с пятью параметрами, два из которых массивы, читается плохо: new Email($to, $subject, '', [], ['inv.pdf']). Перегрузок в языке нет, добавить шестой параметр — значит править все вызовы. Валидация адресов размазывается по тем местам, где письма собирают.
Решение
Разделить продукт Email, неизменяемый и всегда валидный, и строителя EmailBuilder: по методу на каждую часть, вызовы в любом порядке, build() проверяет обязательное и отдаёт продукт. Типовые письма собирает директор Mailings: это рецепты над строителем, чтобы «приветственное письмо» собиралось одинаково во всём приложении.
Строитель нужен, когда у объекта много необязательных частей или сборка идёт в несколько шагов. Если частей две-три, хватит конструктора с именованными аргументами.
Структура
Продукт не знает о строителе и не умеет быть «наполовину собранным»: все поля заданы в конструкторе, списки — копии. В PHP это final readonly, в Python frozen dataclass с проверкой в __post_init__, в TypeScript readonly поля.
Как это собирается
Типовое письмо собирает директор: Mailings::welcome вызывается из регистрации одной строкой, и приветствие везде одинаковое. Письма о заказах собирает обработчик очереди: он берёт строки заказов, создаёт EmailBuilder на каждую и добавляет части по данным - счёт во вложения, если заказ оплачен, менеджера в копию, если он назначен. Только build() отдаёт готовый Email, который уходит в отправку.
Ошибка в данных ловится на шаге, а не после отправки: адрес менеджера без @ бросает исключение уже в cc(), обработчик пропускает это письмо и пишет в лог, остальные заказы уходят. В Go те же шаги - это список опций, который растёт по условиям, а проверка случается один раз в NewEmail.
Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода.
Реализации
Строитель держит состояние сборки и единственную точку проверки. Каждый шаг возвращает $this, поэтому вызовы складываются в цепочку, а порядок шагов не важен: строитель, а не вызывающий код, знает, когда всё готово.
| Язык | Особенность |
|---|---|
| PHP | Классический текучий строитель; обязательные части проверяются в build(), адреса — на входе шага |
| Go | Строитель не нужен: обязательные части — аргументы NewEmail, необязательные — функциональные опции WithCC(...), одна проверка в конструкторе |
| Python | Строитель не нужен: keyword-only аргументы с умолчаниями, продукт проверяет себя в __post_init__ |
| TypeScript | Текучий строитель с this-типом; продукт — интерфейс с readonly |
Запуск: make test T=patterns/creational/builder
Плюсы и минусы
Тесты проверяют четыре свойства: обязательного достаточно, необязательное имеет умолчания; шаги в любом порядке; без обязательного build() падает; директор собирает типовые письма одинаково. Пятый тест — продукт не меняется вместе со строителем: списки копируются при сборке.
- ✅ Читаемая сборка объекта с многими необязательными частями.
- ✅ Продукт всегда валиден: проверка в одном месте, частично собранного объекта не существует.
- ✅ Рецепты (директор) переиспользуются и тестируются отдельно.
- ❌ Второй класс рядом с каждым сложным продуктом; в языках с именованными аргументами часто лишний.
- ❌ Изменяемый строитель легко переиспользовать неправильно: см. практику.
Когда не применять
- Частей мало или все обязательные: конструктор понятнее.
- В языке есть именованные аргументы с умолчаниями (Python, Kotlin, PHP 8): они закрывают большинство случаев.
- Нужны не шаги сборки, а варианты одного объекта: это Прототип или фабрика.
Отличия между языками
- PHP — именованные аргументы с 8.0 закрывают простые случаи, но строитель остаётся полезным, когда части накапливаются (
cc()можно звать много раз) или сборка растянута по коду. - Go — канонический ответ на «много необязательных параметров» — функциональные опции:
NewEmail(to, subject, WithCC(...)). Это строитель без объекта-строителя: шаги — функции, порядок любой, проверка одна. - Python — keyword-only аргументы и
frozendataclass делают приём почти невидимым:email(to, subject, cc=(...,))читается как сборка по шагам, а валидация живёт в продукте. - TypeScript — тип
thisв возвращаемом значении позволяет расширять строитель наследованием, не ломая цепочку. Альтернатива — объектный литерал с необязательными полями и функция-валидатор.
Практика
Учебный строитель изменяемый, и это безопасно, пока им пользуется один вызов. В проекте строители хранят и переиспользуют, и общий «базовый» запрос, который один отчёт незаметно дополнил для другого, — типичный инцидент. Практика: запрос отчёта показывает неизменяемый строитель SQL с параметрами вместо подстановки значений, пагинацией с границами и директором, который делит одну базу между отчётами без риска.
Связанные темы
- Практика: запрос отчёта — неизменяемый строитель, который можно делить
- Абстрактная фабрика (Abstract Factory) — создаёт семейство готовых объектов, строитель — один объект по шагам.