handbookНаписать

patterns/creational/builder/README.md · тема · 4 языка · 23 теста

+ практика: запрос отчёта

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

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

Проблема ​

Письмо состоит из адресата и темы (обязательно), текста, копий и вложений (по желанию). Конструктор с пятью параметрами, два из которых массивы, читается плохо: new Email($to, $subject, '', [], ['inv.pdf']). Перегрузок в языке нет, добавить шестой параметр — значит править все вызовы. Валидация адресов размазывается по тем местам, где письма собирают.

Решение ​

Разделить продукт Email, неизменяемый и всегда валидный, и строителя EmailBuilder: по методу на каждую часть, вызовы в любом порядке, build() проверяет обязательное и отдаёт продукт. Типовые письма собирает директор Mailings: это рецепты над строителем, чтобы «приветственное письмо» собиралось одинаково во всём приложении.

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

Структура ​

uses

builds

Email

+to: string

+subject: string

+body: string

+cc: string[]

+attachments: string[]

EmailBuilder

+to(address) : EmailBuilder

+subject(text) : EmailBuilder

+body(text) : EmailBuilder

+cc(address) : EmailBuilder

+attach(file) : EmailBuilder

+build() : Email

Mailings

+welcome(to, name) : Email

+invoice(to, number, pdf) : Email

Продукт не знает о строителе и не умеет быть «наполовину собранным»: все поля заданы в конструкторе, списки — копии. В 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 аргументы и frozen dataclass делают приём почти невидимым: email(to, subject, cc=(...,)) читается как сборка по шагам, а валидация живёт в продукте.
  • TypeScript — тип this в возвращаемом значении позволяет расширять строитель наследованием, не ломая цепочку. Альтернатива — объектный литерал с необязательными полями и функция-валидатор.

Практика ​

Учебный строитель изменяемый, и это безопасно, пока им пользуется один вызов. В проекте строители хранят и переиспользуют, и общий «базовый» запрос, который один отчёт незаметно дополнил для другого, — типичный инцидент. Практика: запрос отчёта показывает неизменяемый строитель SQL с параметрами вместо подстановки значений, пагинацией с границами и директором, который делит одну базу между отчётами без риска.

Связанные темы ​

© 2014-2026 Тимур ТурдыевСайт и справочник работают на VPS от AdminVPS