Строитель — это порождающий паттерн проектирования, который позволяет создавать объекты пошагово.
В отличие от других порождающих паттернов, Строитель позволяет производить различные продукты, используя один и тот же процесс строительства.
Концептуальный пример
Этот пример показывает структуру паттерна Строитель , а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.
index.php: Пример структуры паттерна
<?php
namespace RefactoringGuru\Builder\Conceptual;
/**
* Интерфейс Строителя объявляет создающие методы для различных частей объектов
* Продуктов.
*/
interface Builder
{
public function producePartA(): void;
public function producePartB(): void;
public function producePartC(): void;
}
/**
* Классы Конкретного Строителя следуют интерфейсу Строителя и предоставляют
* конкретные реализации шагов построения. Ваша программа может иметь несколько
* вариантов Строителей, реализованных по-разному.
*/
class ConcreteBuilder1 implements Builder
{
private $product;
/**
* Новый экземпляр строителя должен содержать пустой объект продукта,
* который используется в дальнейшей сборке.
*/
public function __construct()
{
$this->reset();
}
public function reset(): void
{
$this->product = new Product1();
}
/**
* Все этапы производства работают с одним и тем же экземпляром продукта.
*/
public function producePartA(): void
{
$this->product->parts[] = "PartA1";
}
public function producePartB(): void
{
$this->product->parts[] = "PartB1";
}
public function producePartC(): void
{
$this->product->parts[] = "PartC1";
}
/**
* Конкретные Строители должны предоставить свои собственные методы
* получения результатов. Это связано с тем, что различные типы строителей
* могут создавать совершенно разные продукты с разными интерфейсами.
* Поэтому такие методы не могут быть объявлены в базовом интерфейсе
* Строителя (по крайней мере, в статически типизированном языке
* программирования). Обратите внимание, что PHP является динамически
* типизированным языком, и этот метод может быть в базовом интерфейсе.
* Однако мы не будем объявлять его здесь для ясности.
*
* Как правило, после возвращения конечного результата клиенту, экземпляр
* строителя должен быть готов к началу производства следующего продукта.
* Поэтому обычной практикой является вызов метода сброса в конце тела
* метода getProduct. Однако такое поведение не является обязательным, вы
* можете заставить своих строителей ждать явного запроса на сброс из кода
* клиента, прежде чем избавиться от предыдущего результата.
*/
public function getProduct(): Product1
{
$result = $this->product;
$this->reset();
return $result;
}
}
/**
* Имеет смысл использовать паттерн Строитель только тогда, когда ваши продукты
* достаточно сложны и требуют обширной конфигурации.
*
* В отличие от других порождающих паттернов, различные конкретные строители
* могут производить несвязанные продукты. Другими словами, результаты различных
* строителей могут не всегда следовать одному и тому же интерфейсу.
*/
class Product1
{
public $parts = [];
public function listParts(): void
{
echo "Product parts: " . implode(', ', $this->parts) . "\n\n";
}
}
/**
* Директор отвечает только за выполнение шагов построения в определённой
* последовательности. Это полезно при производстве продуктов в определённом
* порядке или особой конфигурации. Строго говоря, класс Директор необязателен,
* так как клиент может напрямую управлять строителями.
*/
class Director
{
/**
* @var Builder
*/
private $builder;
/**
* Директор работает с любым экземпляром строителя, который передаётся ему
* клиентским кодом. Таким образом, клиентский код может изменить конечный
* тип вновь собираемого продукта.
*/
public function setBuilder(Builder $builder): void
{
$this->builder = $builder;
}
/**
* Директор может строить несколько вариаций продукта, используя одинаковые
* шаги построения.
*/
public function buildMinimalViableProduct(): void
{
$this->builder->producePartA();
}
public function buildFullFeaturedProduct(): void
{
$this->builder->producePartA();
$this->builder->producePartB();
$this->builder->producePartC();
}
}
/**
* Клиентский код создаёт объект-строитель, передаёт его директору, а затем
* инициирует процесс построения. Конечный результат извлекается из объекта-
* строителя.
*/
function clientCode(Director $director)
{
$builder = new ConcreteBuilder1();
$director->setBuilder($builder);
echo "Standard basic product:\n";
$director->buildMinimalViableProduct();
$builder->getProduct()->listParts();
echo "Standard full featured product:\n";
$director->buildFullFeaturedProduct();
$builder->getProduct()->listParts();
// Помните, что паттерн Строитель можно использовать без класса Директор.
echo "Custom product:\n";
$builder->producePartA();
$builder->producePartC();
$builder->getProduct()->listParts();
}
$director = new Director();
clientCode($director);
Output.txt: Результат выполнения
Standard basic product:
Product parts: PartA1
Standard full featured product:
Product parts: PartA1, PartB1, PartC1
Custom product:
Product parts: PartA1, PartC1
Пример из реальной жизни
Одним из лучших применений паттерна Строитель является конструктор запросов SQL. Интерфейс Строителя определяет общие шаги, необходимые для построения основного SQL-запроса. В тоже время Конкретные Строители, соответствующие различным диалектам SQL, реализуют эти шаги, возвращая части SQL-запросов, которые могут быть выполнены в данном движке базы данных.
index.php: Пример из реальной жизни
<?php
namespace RefactoringGuru\Builder\RealWorld;
/**
* Интерфейс Строителя объявляет набор методов для сборки SQL-запроса.
*
* Все шаги построения возвращают текущий объект строителя, чтобы обеспечить
* цепочку: $builder->select(...)->where(...)
*/
interface SQLQueryBuilder
{
public function select(string $table, array $fields): SQLQueryBuilder;
public function where(string $field, string $value, string $operator = '='): SQLQueryBuilder;
public function limit(int $start, int $offset): SQLQueryBuilder;
// +100 других методов синтаксиса SQL...
public function getSQL(): string;
}
/**
* Каждый Конкретный Строитель соответствует определённому диалекту SQL и может
* реализовать шаги построения немного иначе, чем остальные.
*
* Этот Конкретный Строитель может создавать SQL-запросы, совместимые с MySQL.
*/
class MysqlQueryBuilder implements SQLQueryBuilder
{
protected $query;
protected function reset(): void
{
$this->query = new \stdClass();
}
/**
* Построение базового запроса SELECT.
*/
public function select(string $table, array $fields): SQLQueryBuilder
{
$this->reset();
$this->query->base = "SELECT " . implode(", ", $fields) . " FROM " . $table;
$this->query->type = 'select';
return $this;
}
/**
* Добавление условия WHERE.
*/
public function where(string $field, string $value, string $operator = '='): SQLQueryBuilder
{
if (!in_array($this->query->type, ['select', 'update', 'delete'])) {
throw new \Exception("WHERE can only be added to SELECT, UPDATE OR DELETE");
}
$this->query->where[] = "$field $operator '$value'";
return $this;
}
/**
* Добавление ограничения LIMIT.
*/
public function limit(int $start, int $offset): SQLQueryBuilder
{
if (!in_array($this->query->type, ['select'])) {
throw new \Exception("LIMIT can only be added to SELECT");
}
$this->query->limit = " LIMIT " . $start . ", " . $offset;
return $this;
}
/**
* Получение окончательной строки запроса.
*/
public function getSQL(): string
{
$query = $this->query;
$sql = $query->base;
if (!empty($query->where)) {
$sql .= " WHERE " . implode(' AND ', $query->where);
}
if (isset($query->limit)) {
$sql .= $query->limit;
}
$sql .= ";";
return $sql;
}
}
/**
* Этот Конкретный Строитель совместим с PostgreSQL. Хотя Postgres очень похож
* на Mysql, в нем всё же есть ряд отличий. Чтобы повторно использовать общий
* код, мы расширяем его от строителя MySQL, переопределяя некоторые шаги
* построения.
*/
class PostgresQueryBuilder extends MysqlQueryBuilder
{
/**
* Помимо прочего, PostgreSQL имеет несколько иной синтаксис LIMIT.
*/
public function limit(int $start, int $offset): SQLQueryBuilder
{
parent::limit($start, $offset);
$this->query->limit = " LIMIT " . $start . " OFFSET " . $offset;
return $this;
}
// + тонны других переопределений...
}
/**
* Обратите внимание, что клиентский код непосредственно использует объект
* строителя. Назначенный класс Директора в этом случае не нужен, потому что
* клиентский код практически всегда нуждается в различных запросах, поэтому
* последовательность шагов конструирования непросто повторно использовать.
*
* Поскольку все наши строители запросов создают продукты одного типа (это
* строка), мы можем взаимодействовать со всеми строителями, используя их общий
* интерфейс. Позднее, если мы реализуем новый класс Строителя, мы сможем
* передать его экземпляр существующему клиентскому коду, не нарушая его,
* благодаря интерфейсу SQLQueryBuilder.
*/
function clientCode(SQLQueryBuilder $queryBuilder)
{
// ...
$query = $queryBuilder
->select("users", ["name", "email", "password"])
->where("age", 18, ">")
->where("age", 30, "<")
->limit(10, 20)
->getSQL();
echo $query;
// ...
}
/**
* Приложение выбирает подходящий тип строителя запроса в зависимости от текущей
* конфигурации или настроек среды.
*/
// if ($_ENV['database_type'] == 'postgres') {
// $builder = new PostgresQueryBuilder(); } else {
// $builder = new MysqlQueryBuilder(); }
//
// clientCode($builder);
echo "Testing MySQL query builder:\n";
clientCode(new MysqlQueryBuilder());
echo "\n\n";
echo "Testing PostgresSQL query builder:\n";
clientCode(new PostgresQueryBuilder());
Output.txt: Результат выполнения
Testing MySQL query builder:
SELECT name, email, password FROM users WHERE age > '18' AND age < '30' LIMIT 10, 20;
Testing PostgresSQL query builder:
SELECT name, email, password FROM users WHERE age > '18' AND age < '30' LIMIT 10 OFFSET 20;