Строитель

Строитель — это порождающий паттерн проектирования, который позволяет создавать сложные объекты пошагово, отделяя код конструирования от представления самого объекта.
Проблема
Продолжим автомобильную тему из статьи про Абстрактную фабрику. У машины помимо двигателя и шин есть набор опций: люк, GPS-навигатор, кожаный салон — и не все они обязательны.
Самое очевидное решение — добавить всё в конструктор:
class Car
{
public function __construct(
public readonly string $engine,
public readonly string $tires,
public readonly bool $hasSunroof = false,
public readonly bool $hasGps = false,
public readonly bool $hasLeatherSeats = false,
public readonly bool $hasParkingSensors = false,
) {}
}При вызове такой конструктор превращается в нечитаемый набор булевых флагов — «телескопический конструктор»:
$car = new Car('V8', 'Michelin', true, false, true, false);Что здесь true, а что false? Чтобы понять, нужно заглянуть в сигнатуру конструктора. Стоит поменять местами два соседних параметра — и получим машину с люком вместо парктроников, а компилятор об этом не предупредит.
Решение
Строитель выносит процесс сборки в отдельный объект с понятными пошаговыми методами. Сначала опишем интерфейс строителя:
interface CarBuilder
{
public function setEngine(string $engine): static;
public function setTires(string $tires): static;
public function addSunroof(): static;
public function addGps(): static;
public function addLeatherSeats(): static;
public function build(): Car;
}И его конкретную реализацию, которая накапливает состояние и в конце собирает готовый объект:
class StandardCarBuilder implements CarBuilder
{
private string $engine = 'Base';
private string $tires = 'Standard';
private bool $hasSunroof = false;
private bool $hasGps = false;
private bool $hasLeatherSeats = false;
public function setEngine(string $engine): static
{
$this->engine = $engine;
return $this;
}
public function setTires(string $tires): static
{
$this->tires = $tires;
return $this;
}
public function addSunroof(): static
{
$this->hasSunroof = true;
return $this;
}
public function addGps(): static
{
$this->hasGps = true;
return $this;
}
public function addLeatherSeats(): static
{
$this->hasLeatherSeats = true;
return $this;
}
public function build(): Car
{
return new Car(
$this->engine,
$this->tires,
$this->hasSunroof,
$this->hasGps,
$this->hasLeatherSeats,
);
}
}Каждый метод возвращает $this, поэтому вызовы можно выстроить в цепочку — сразу видно, что именно собирается:
$car = (new StandardCarBuilder())
->setEngine('V8')
->setTires('Michelin')
->addSunroof()
->addLeatherSeats()
->build();Никаких безымянных true/false — каждая опция называется явно, а порядок вызовов не влияет на результат.
Директор: переиспользование сценариев сборки
Если в приложении есть типовые конфигурации — например, «спортивная» и «семейная» комплектации — последовательность вызовов удобно вынести в отдельный класс, директора:
class CarDirector
{
public function buildSportsCar(CarBuilder $builder): Car
{
return $builder
->setEngine('V8 Turbo')
->setTires('Performance')
->build();
}
public function buildFamilyCar(CarBuilder $builder): Car
{
return $builder
->setEngine('Hybrid')
->setTires('Standard')
->addGps()
->addLeatherSeats()
->build();
}
}Директор не обязателен — это лишь способ не дублировать одну и ту же последовательность вызовов строителя в разных местах клиентского кода:
$director = new CarDirector();
$builder = new StandardCarBuilder();
$sportsCar = $director->buildSportsCar($builder);Из чего состоит паттерн
- Продукт (
Car) — сложный объект, который в итоге нужно получить. - Строитель (
CarBuilder) — интерфейс с методами для настройки каждой части продукта и методомbuild(), возвращающим готовый результат. - Конкретный строитель (
StandardCarBuilder) — хранит промежуточное состояние и умеет собрать из него продукт. - Директор (
CarDirector, опционально) — знает типовые последовательности вызовов строителя для повторяющихся конфигураций.
Строитель vs Абстрактная фабрика
Оба паттерна создают составные объекты, но по-разному:
| Абстрактная фабрика | Строитель | |
|---|---|---|
| Что получаем | Семейство готовых продуктов | Один сложный продукт |
| Как получаем | Одним вызовом метода фабрики | Пошагово, через цепочку вызовов |
| Промежуточное состояние | Отсутствует | Накапливается внутри строителя |
| Обязательность шагов | Все продукты создаются всегда | Часть шагов может быть опциональной |
Абстрактная фабрика отвечает за то, какое семейство продуктов совместимо друг с другом. Строитель отвечает за то, как пошагово собрать один продукт, у которого много независимых опциональных частей.
Когда применять
- У объекта много опциональных параметров, и телескопический конструктор становится нечитаемым.
- Объект нужно собирать в несколько шагов, а промежуточные состояния сборки не должны быть видны клиенту.
- Один и тот же процесс сборки должен уметь производить разные представления объекта (например,
Carи его DTO для API — оба через общий интерфейс строителя, но разными конкретными строителями).
Плюсы и минусы
Строитель делает код сборки читаемым и явным, позволяет собирать объект частями и переиспользовать сценарии сборки через директора. Валидация обязательных полей может происходить в одном месте — методе build().
Минус — усложнение кода: вместо одного конструктора появляются интерфейс строителя, как минимум одна его реализация и, возможно, директор. Для объекта с двумя-тремя простыми полями это избыточно.
Итог
Строитель полезен ровно тогда, когда объект действительно сложный: у него много опциональных частей, а порядок или комбинация этих частей должны быть явными и защищёнными от ошибок. Если объект простой — обычного конструктора или именованных аргументов PHP вполне достаточно.