
Строитель на PHP
Строитель — это порождающий паттерн проектирования, который позволяет создавать объекты пошагово.
В отличие от других порождающих паттернов, Строитель позволяет производить различные продукты, используя один и тот же процесс строительства.
Сложность:
Популярность:
Применимость: Паттерн можно часто встретить в PHP-коде, особенно там, где требуется пошаговое создание продуктов или конфигурация сложных объектов.
Признаки применения паттерна: Строителя можно узнать в классе, который имеет один создающий метод и несколько методов настройки создаваемого продукта. Обычно, методы настройки вызывают для удобства цепочкой (например, someBuilder->setValueA(1)->setValueB(2)->create()
).
Концептуальный пример
Этот пример показывает структуру паттерна Строитель, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире 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;