Строитель (Builder) — шаблон проектирования

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

Пример задачи: создание запросов к БД, используя конструктор запросов.

Под сложным объектом понимается объект с заполненными свойствами и разветлённой логикой поведения, зависящей от этих свойств и контекста. Чтобы вынести заполнение свойств за пределы пользовательского кода или же облегчить заполнение свойств с пользовательской стороны, то используется класс-строитель. 

В паттерне фигурируют следующие понятия: 
  • Строитель – класс, который инкапсулирует внутри себя пошаговое создание продукта. На каждом шаге в будущий продукт добавляется какая-то значимая часть, а в конце производится финишная сборка продукта;
  • Директор – класс, который управляет строителем. Директор требуется в тех случаях, когда нужно перенести процесс поэтапной сборки продукта строителем в какую-то логическую сущность. В иных случаях роль директора выполняет пользовательский код;
  • Продукт – то, что создаёт строитель. Это может быть объект класса, строка, число или  переменная любого другого типа данных.

Далее приводится реализация паттерна на языке PHP. Скачать исходники (ZIP, 3 Кб)

sql_classes.php

Содержит классы для формирования запросов в базы данных:
  • SQLQuery — абстрактный класс для работы с запросами абстрактной реляционной базы данных;
  • SQLQueryMYSQL — конечный класс для работы с запросами БД MySQL. Представляет собой продукт строителя;
  • SQLQueryMSSQL — конечный класс для работы с запросами БД MS SQL. Представляет собой продукт строителя.
<?php

/**
 * Абстрактный класс для создания запроса в произвольную БД
 */
abstract class SQLQuery
{
    /**
     * Операция запроса. Допускаются значения: 'SELECT', 'INSERT', 'UPDATE', 'DELETE'
     * @var string
     */
    public $operation;

    /**
     * Название таблицы, в отношении которой выполняется запрос
     * @var string
     */
    public $table;

    /**
     * Перечень столбцов, в отношении которых выполняется запрос
     * @var string
     */
    public $columns;

    /**
     * Условия запроса
     * @var string
     */
    public $where;

    /**
     * Параметры сортировки
     * @var string
     */
    public $order;

    /**
     * Количество элементов, подпадающих под действие запроса.
     * Например, количество элементов попадающих в выборку SELECT
     * @var int
     */
    public $countElements;

    public function __construct()
    {
        $this->reset();
    }

    /**
     * Очищает свойства объекта запроса, подгатавливая его для нового запроса
     */
    public function reset()
    {
        $this->operation = '';
        
        $this->table = '';
        $this->columns = '';
        $this->where = '';
        $this->order = '';
        $this->countElements = 0;
    }

    /**
     * Получение строки с готовым запросом
     * @return string
     */
    abstract public function getString();
}

/**
 * Класс для создания запроса в БД MySQL
 */
class SQLQueryMYSQL extends SQLQuery
{
    /**
     * Получение строки с готовым запросом
     * @return string
     */
    public function getString()
    {
        $query = '';

        //Операция выборки SELECT
        if ($this->operation == 'SELECT') {
            $query = 'SELECT';

            //Поля выборки
            if ($this->columns == '') {
                $query.= ' *';
            } else {
                $query.= ' '.$this->columns;
            }

            //Таблица
            $query .= ' FROM '.$this->table;

            //Условия
            if ($this->where != '') {
                $query .= ' WHERE '.$this->where;
            }

            //Параметры сортировки
            if ($this->order != '') {
                $query .= ' ORDER BY '.$this->order;
            }

            //Ограничитель количества элементов выборки
            if ($this->countElements > 0) {
                $query .= ' LIMIT 0, '.$this->countElements;
            }
        }

        return $query;
    }
}

/**
 * Класс для создания запроса в БД MSSQL
 */
class SQLQueryMSSQL extends SQLQuery
{
    /**
     * Получение строки с готовым запросом
     * @return string
     */
    public function getString(): string
    {
        $query = '';

        //Операция выборки SELECT
        if ($this->operation == 'SELECT') {
            $query = 'SELECT';

            //Ограничитель выборки
            if ($this->countElements > 0) {
                $query .= ' TOP '.$this->countElements;
            }

            //Поля выборки
            if ($this->columns == '') {
                $query.= ' *';
            } else {
                $query.= ' '.$this->columns;
            }

            //Таблица
            $query .= ' FROM '.$this->table;

            //Условия
            if ($this->where != '') {
                $query .= ' WHERE '.$this->where;
            }

            //Параметры сортировки
            if ($this->order != '') {
                $query .= ' ORDER BY '.$this->order;
            }
        }

        return $query;
    }
}

?>

Обратите внимание, что базы MySQL и MS SQL имеют различия в синтаксисте оператора SELECT. Например, запрос на выборку 10 элементов таблицы table1 в MySQL будет выглядеть так:
SELECT * FROM table1 LIMIT 10

А для MS SQL запрос выглядит следующим образом:
SELECT TOP 10 * FROM table1

sql_builders.php

Содержит абстрактный класс строителя BuilderSQLQuery, который включает в себя:
  • Защищённое свойство $query – продукт строителя и он же объект запроса;
  • Методы select, from, where, order, count для наполнения объекта $query различными параметрами. Обратите внимание, что каждый из данных методов возвращает объект текущего строителя return $this; Это сделано для того, чтобы методы можно было вызывать друг за другом;
  • Метод reset для очистки параметров объекта запроса при формировании нового запроса;
  • Метод getQuery для получения объекта запроса (продукта).

Так же файл содержит, наследованные от абстрактного строителя BuilderSQLQuery, классы строителей BuilderSQLQueryMySQL (для построения запроса к MySQL) и BuilderSQLQueryMSSQL (для построения запроса к MS SQL).
<?php

//Подключение классов запросов для работы с БД
require_once(__DIR__.'/sql_classes.php');

/**
 * Абстрактный класс конструктора запросов к различным видам БД
 */
abstract class BuilderSQLQuery
{
    /**
     * Объект запроса
     * @var SQLQuery
     */
    protected $query;

    /**
     * Конструктор конструктора.
     * В конечном классе тут создаётся объект запроса для конкретного вида БД
     */
    public function __construct()
    {
        /*
        Образец вызова (вместо SQLQuery укажите класс для работы с конкртеной БД):
        $this->query = new SQLQuery();
        */
    }

    /**
     * Возвращает объект запроса
     * @return SQLQuery;
     */
    public function getQuery()
    {
        return $this->query;
    }

    /**
     * Устанавка операции выборки SELECT и указание колонок выборки
     * @param string $columns - колонки, которые будут возвращены в выборке. Если указать пустое значение, то будут возвращены все колонки
     * @return BuilderSQLQuery
     */
    public function select($columns = '')
    {
        $this->query->operation = 'SELECT';
        $this->query->columns = $columns;

        return $this;
    }

    /**
     * Установка таблицы, из которой происходит выборки
     * @param string $table - таблица выборки
     * @return BuilderSQLQuery
     */
    public function from($table)
    {
        $this->query->table = $table;
        return $this;
    }

    /**
     * Установка условий запроса
     * @param string $where - строка с условиями запроса
     * @return BuilderSQLQuery
     */
    public function where($where)
    {
        $this->query->where = $where;
        return $this;
    }

    /**
     * Установка параметров сортировки
     * @param string $order - строка параметрами сортировки запроса
     * @return BuilderSQLQuery
     */
    public function order($order)
    {
        $this->query->order = $order;
        return $this;
    }

    /**
     * Установка количества элементов, подпадающих под действие запроса.
     * Например, количество элементов попадающих в выборку SELECT
     * 
     * @param int|string $count - количество элементов
     * @return BuilderSQLQuery
     */
    public function count($count)
    {
        $this->query->countElements = (int)$count;
        return $this;
    }

    /**
     * Очищает свойства объекта запроса, подгатавливая его для нового запроса
     * @return BuilderSQLQuery
     */
    public function reset()
    {
        $this->reset();
        return $this;
    }
}

/**
 * Конструктор запроса в БД MySQL
 */
class BuilderSQLQueryMySQL extends BuilderSQLQuery
{
    public function __construct()
    {
        $this->query = new SQLQueryMySQL();
    }
}

/**
 * Конструктор запроса в БД MSSQL
 */
class BuilderSQLQueryMSSQL extends BuilderSQLQuery
{
    public function __construct()
    {
        $this->query = new SQLQueryMSSQL();
    }
}

?>

test.php

Тестовый скрипт, в котором с помощью строителя BuilderSQLQueryMySQL выполняется создание запроса к MySQL и затем с помощью метода getQuery объект запроса помещается в переменную $queryMySql. По аналогии, но с помощью строителя BuilderSQLQueryMSSQL, происходит создание запроса к MS SQL. В конце файла, используя метод getString, получаем текст sql-запросов и выводим их на экран.

Как можно увидеть, порядок работы с MySQL и MS SQL у строителей BuilderSQLQueryMySQL и BuilderSQLQueryMSSQL одинаковый.
<?php

//Подключение конструкторов запросов для работы с БД
require_once(__DIR__.'/sql_builders.php');

//Запрос в БД MySQL
$queryMySql = (new BuilderSQLQueryMySQL())
              ->select('id, name')
              ->from('clients')
              ->where('age >= 18')
              ->order('name ASC')
              ->count(50)
              ->getQuery();

//Запрос в БД MSSQL
$queryMSSql = (new BuilderSQLQueryMSSQL())
              ->select('id, name')
              ->from('clients')
              ->where('age >= 18')
              ->order('name ASC')
              ->count(50)
              ->getQuery();

$queryStr = $queryMySql->getString();
echo "Запрос на MySQL:\n".$queryStr."\n\n";

$queryStr = $queryMSSql->getString();
echo "Запрос на Microsoft SQL:\n".$queryStr."\n\n";

?>

Результат работы скрипта test.php
Запрос на MySQL:
SELECT id, name FROM clients WHERE age >= 18 ORDER BY name ASC LIMIT 0, 50

Запрос на Microsoft SQL:
SELECT TOP 50 id, name FROM clients WHERE age >= 18 ORDER BY name ASC

А где же директор? В тестовом скрипте роль директора выполняет пользовательский код, который поочерёдно вызывает методы select, from, where, order, count, заполняя свойства продукта, и затем с помощью getQuery возвращается объект продукта:
//Запрос в БД MSSQL
$queryMSSql = (new BuilderSQLQueryMySQL())
              ->select('id, name')
              ->from('clients')
              ->where('age >= 18')
              ->order('name ASC')
              ->count(50)
              ->getQuery();

Так как sql-запросы обычно разные и редко повторяются в разных местах проекта, то для данной задачи нет необходимости создавать отдельный класс директора, чтобы вынести поэтапную сборку продукта строителем в какую-то логическую сущность. Но если указанный запрос на получение 50 клиентов старше 18 лет  используется в нескольких местах программы и чтобы каждый раз в пользовательском коде не повторять длинные конструкции, то есть смысл добавить отдельный класс директора, который внутри себя будет поэтапно вызывать методы строителя select, from, where, order, count и возвращать объект запроса. В итоге создание запроса примет вид:
$director = new DirectorBilderSQL();
$queryMSSql = $director->getClients18AndMore(new BuilderSQLQueryMSSQL(), 50);

Код метода getClients18AndMore
/**
 * Возвращает объекта запроса на выборку клиентов, возрастом от 18 лет и старше
 * @param BuilderSQLQuery $builderSQL - конструктор запроса (объект-строитель)
 * @param int $count - максимальное количество клиентов, которые должны попасть в выборку
 * @return SQLQuery - вернёт объект запроса
 */
public function getClients18AndMore($builderSQL, $count)
{
    return $builderSQL->select('id, name')
            ->from('clients')
            ->where('age >= 18')
            ->order('name ASC')
            ->count(50)
            ->getQuery($count);
}