Перейти к содержимому
Фёдор Башинский

Строитель

4 min read
Обложка статьи «Строитель»

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

Проблема

Продолжим автомобильную тему из статьи про Абстрактную фабрику. У машины помимо двигателя и шин есть набор опций: люк, 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 вполне достаточно.