Строитель (англ. Builder)— это порождающий шаблон проектирования, который позволяет создавать сложные объекты пошагово. Строитель даёт возможность использовать один и тот же код строительства для получения разных представлений объектов.
Строитель предлагает вынести конструирование объекта за пределы его собственного класса, поручив это дело отдельным объектам, называемым строителями. При этом, промежуточный результат всегда остаётся защищён.
Шаблон предлагает разбить процесс конструирования объекта на отдельные шаги (например, построитьСтены , вставитьДвери и другие). Чтобы создать объект, вам нужно поочерёдно вызывать методы строителя. Причём не нужно запускать все шаги, а только те, что нужны для производства объекта определённой конфигурации.
Строитель позволяет создавать сложные объекты пошагово.
Реализация в JavaScript
Строитель - позволяет создавать сложные объекты пошагово. Строитель позволяет использовать один код для получения разных объектов.
Зачем использовать - отделить сложную логику создания от финального представления.
class ProductBuilder { constructor() { this.name = 'A Product'; this.price = 9.99; this.category = 'other'; } withName(name) { this.name = name; return this; } withPrice(price) { this.price = price; return this; } withCategory(category) { this.category = category; return this; } build() { return { name: this.name, price: this.price, category: this.category, } } } console.log( new ProductBuilder() .withName('Harry Potter') .withCategory('book') .build() ) // => // { // name: 'Harry Potter', // price: 9.99, // category: 'book' // }
Обратите внимание, что при создании экземпляра мы не используем сложный вызов в конструкторе. Это нерационально, если, например, у вас объект имеет под 100 свойство.
Если объект простой, то шаблон строитель усложнит код.
В чем отличие строителя от фабрики: шаблон строитель позволяет конфигурировать объект, фабрика же создает объект за один шаг. То есть фабрика, как правило, не конфигурирует объект при его создании.
Вот еще пример с более высокой абстракцией director...
Допустим, на сайте присутствует баннер. В зависимости от размера окна, в котором отображается сайт, баннер имеет разные характеристики: ширину, высоту, изображение. Со временем могут появиться новые характеристи, это следует учесть, чтобы сделать легко обслуживаемый код. Решим задачу с помощью паттерна "Строитель".
Для начала создадим класс баннера.
class Banner { constructor(params) { this.url = params.url; this.lowResImg = params.lowResImg; this.highResImg = params.highResImg; } setWidth(width) { this.width = width; } setHeight(height) { this.height = height; } setImg(img) { this.currentImg = img; } render() { let banner = document.getElementById("banner"); banner.style.width = this.width + "px"; banner.style.height = this.height + "px"; banner.setAttribute('src', this.currentImg); banner.setAttribute('data-url', this.url); document.body.appendChild(banner); } }
В нашем примере, класс Banner имеет метод render()
, который получает элемент с id="banner"
и отрисовывает его в зависимости от параметров.
Теперь создадим базовый строитель и реализации строителей для низкого и высокого разрешения.
class BuilderBanner { constructor(params) { this.banner = new Banner(params); } renderBanner() { this.banner.render(); } } class BuilderLowResBanner extends BuilderBanner { setWidth(width) { this.banner.setWidth(320); } setHeight(height) { this.banner.setHeight(100); } setImg(img) { this.banner.setImg(this.banner.lowResImg); } } class BuilderHighResBanner extends BuilderBanner { setWidth(width) { this.banner.setWidth(820); } setHeight(height) { this.banner.setHeight(400); } setImg(img) { this.banner.setImg(this.banner.highResImg); } }
Осталось реализовать директора, для управления строителями.
class DirectorBanner { constructor(builder) { if (! builder instanceof BuilderBanner) { throw "not builder object passed"; } this.builder = builder; } constructBanner() { this.builder.setWidth(); this.builder.setHeight(); this.builder.setImg(); } renderBanner() { this.builder.renderBanner(); } }
Теперь можно написать реализацию работы с данными классами. Так выглядит клиентский код:
initBanner(); // первичная инициализация window.onresize = () => initBanner(); // перерисовка при масштабировании страницы //функция инициализации: function initBanner() { const bannerParams = { url: "/some-page.html", lowResImg: "awesome-mob.jpeg", highResImg: "avesome-desc.jpeg" }; let bulder = window.innerWidth > 819 ? new BuilderHighResBanner(bannerParams) : new BuilderLowResBanner(bannerParams); let director = new DirectorBanner(bulder); director.constructBanner(); director.renderBanner(); }
Можно было бы всю логику реализовать без разбивки на классы. Тогда не пришлось бы создать несколько уровней абстракции в виде классов баннера, строителей, и директора. Возможно, мы бы в таком случае потратили меньше времени на реализацию данной задачи. Но паттерны окупаются в долгосрочном периоде. В реальных задачах данных больше, а структуры сложнее.
В неструктурированном коде сложно разобраться и поддерживать. Шаблон "Строитель" значительно упрощает клиентскую часть кода, при этом его классы и методы имеют ясное назначение и в них будет легко разобраться и внести необходимые изменение даже спустя длительное время.
Реализация в PHP
namespace DesignPatterns\Creational\Builder; use DesignPatterns\Creational\Builder\Parts\Vehicle; /** * Director is part of the builder pattern. It knows the interface of the builder * and builds a complex object with the help of the builder * * You can also inject many builders instead of one to build more complex objects */ class Director { public function build(Builder $builder): Vehicle { $builder->createVehicle(); $builder->addDoors(); $builder->addEngine(); $builder->addWheel(); return $builder->getVehicle(); } }
Класс Director - это ключевой класс шаблона строитель, задача которого определить метод строителя.
Следующий элемент шаблона - интерфейс Builder.php
namespace DesignPatterns\Creational\Builder; use DesignPatterns\Creational\Builder\Parts\Vehicle; interface Builder { public function createVehicle(); public function addWheel(); public function addEngine(); public function addDoors(); public function getVehicle(): Vehicle; }
TruckBuilder.php
namespace DesignPatterns\Creational\Builder; use DesignPatterns\Creational\Builder\Parts\Vehicle; class TruckBuilder implements Builder { /** * @var Parts\Truck */ private $truck; public function addDoors() { $this->truck->setPart('rightDoor', new Parts\Door()); $this->truck->setPart('leftDoor', new Parts\Door()); } public function addEngine() { $this->truck->setPart('truckEngine', new Parts\Engine()); } public function addWheel() { $this->truck->setPart('wheel1', new Parts\Wheel()); $this->truck->setPart('wheel2', new Parts\Wheel()); $this->truck->setPart('wheel3', new Parts\Wheel()); $this->truck->setPart('wheel4', new Parts\Wheel()); } public function createVehicle() { $this->truck = new Parts\Truck(); } public function getVehicle(): Vehicle { return $this->truck; } }
CarBuilder.php
namespace DesignPatterns\Creational\Builder; use DesignPatterns\Creational\Builder\Parts\Vehicle; class CarBuilder implements Builder { /** * @var Parts\Car */ private $car; public function addDoors() { $this->car->setPart('rightDoor', new Parts\Door()); $this->car->setPart('leftDoor', new Parts\Door()); $this->car->setPart('trunkLid', new Parts\Door()); } public function addEngine() { $this->car->setPart('engine', new Parts\Engine()); } public function addWheel() { $this->car->setPart('wheelLF', new Parts\Wheel()); $this->car->setPart('wheelRF', new Parts\Wheel()); $this->car->setPart('wheelLR', new Parts\Wheel()); $this->car->setPart('wheelRR', new Parts\Wheel()); } public function createVehicle() { $this->car = new Parts\Car(); } public function getVehicle(): Vehicle { return $this->car; } }
Parts/Vehicle.php
namespace DesignPatterns\Creational\Builder\Parts; abstract class Vehicle { /** * @var object[] */ private $data = []; /** * @param string $key * @param object $value */ public function setPart($key, $value) { $this->data[$key] = $value; } }
Parts/Truck.php
namespace DesignPatterns\Creational\Builder\Parts; class Truck extends Vehicle { }
Parts/Car.php
namespace DesignPatterns\Creational\Builder\Parts; class Car extends Vehicle { }
Parts/Engine.php
namespace DesignPatterns\Creational\Builder\Parts; class Engine { }
Parts/Wheel.php
declare(strict_types=1); namespace DesignPatterns\Creational\Builder\Parts; class Wheel { }
Parts/Door.php
namespace DesignPatterns\Creational\Builder\Parts; class Door { }
Тест. Tests/DirectorTest.php
declare(strict_types=1); namespace DesignPatterns\Creational\Builder\Tests; use DesignPatterns\Creational\Builder\Parts\Car; use DesignPatterns\Creational\Builder\Parts\Truck; use DesignPatterns\Creational\Builder\TruckBuilder; use DesignPatterns\Creational\Builder\CarBuilder; use DesignPatterns\Creational\Builder\Director; use PHPUnit\Framework\TestCase; class DirectorTest extends TestCase { public function testCanBuildTruck() { $truckBuilder = new TruckBuilder(); $newVehicle = (new Director())->build($truckBuilder); $this->assertInstanceOf(Truck::class, $newVehicle); } public function testCanBuildCar() { $carBuilder = new CarBuilder(); $newVehicle = (new Director())->build($carBuilder); $this->assertInstanceOf(Car::class, $newVehicle); } }