Шаблонный метод (Template Method) — шаблон проектирования

6 февраля 2020
Шаблон предлагает схему, в которой общий порядок работы объекта и каркас алгоритма задаётся в базовом классе, а в дочерних классах объект и алгоритм получают законченную форму, при этом общая логика, намеченная в базовом классе, не нарушается.

Под алгоритмом тут понимается выполнение какого-то действия с помощью метода, определённого в базовом классе и который является окончательным (в PHP может быть помечен ключевым словом final). В терминологии шаблона данный окончательный метод и есть шаблонный метод. Дочерние же классы реализуют другие методы, используемые шаблонным методом и тем самым в дочернем классе объект получает собственную реализацию алгоритма.

ПРИМЕР ЗАДАЧИ: сделать получение цен покупки зерновых культур (пшеница, рожь, ячмень и т.д.) с сайтов и API скупщиков (экспортёров, переработчиков).

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

Шаблон состоит из двух элементов:
  • Базовый класс – абстрактный класс, у которого определён шаблонный (окончательный/финальный) метод и есть абстрактные методы, используемые шаблонным методом;
  • Дочерний класс – наследник базового класса, реализующий его абстрактные методы. Тем самым алгоритм дочерних классов получает собственную реализацию, укладывающуюся в общую концепцию алгоритма базового класса;

Теперь перейдём к коду.

abstract_products_buyer.class.php

Файл содержит базовый абстрактный класс AbstractProductsBuyer, представляющий товарного скупщика. Класс состоит из следующих свойств и методов:
  • $name – имя или название скупщика;
  • getContentWithProducts() – подключается к сайту/API скупщика и возвращает строку с полученной HTML-страницей товаров (в случае сайта) или массив товаров (в случае API). Абстрактный метод;
  • parseProducts($contentWithProducts) – ищет в строке или массиве товары и их цены, а затем объединяет полученные данные в итоговый стандартизированный массив. Абстрактный метод;
  • savePriceBuy($productName, $price, $weight = '1 тонна') – выполняет сохранение закупочных цен в базу данных. В целях демонстрации метод выводит на экран сообщение о сохранении цены товара;
  • downloadPrices() – скачивает цены закупаемых товаров (получает цены скупщика и сохраняет их у себя в базе данных). Центральный метод, реализующий общий алгоритм класса.

Здесь определён шаблонный метод downloadPrices. Внутри себя он вызывает методы дочерних классов getContentWithProducts() и parseProducts($contentWithProducts), поэтому несмотря на то, что код шаблонного метода для всех подклассов одинаковый, у каждого дочернего класса будет собственная реализация используемых им методов.
<?

/**
 * Абстрактный класс товарного скупщика
 */
abstract class AbstractProductsBuyer
{
    protected $name = '';

    /**
     * Подключается к сайту/API скупщика и получает HTML-содержимое страницы товаров (в случае сайта) или массив товаров (в случае API)
     * @return array|string - возвращает HTML-строку c товарами (в случае сайта) или массив товаров (в случае API)
     */
    abstract public function getContentWithProducts();

    /**
     * Ищет в исходной строке или массиве товары и их цены, затем объединяет полученные данные в итоговый стандартизированный массив
     * @param array|string $contentProducts -  строка, содержащая HTML-список товаров (в случае сайта) или массив товаров (в случае API)
     * @return array - вернёт массив товаров
     */
    abstract public function parseProducts($contentWithProducts);

    /**
     * Сохранение цены товара
     * 
     * @param string $productName - название товара
     * @param int $price - стоимость товара
     * @param string $weight - вес товара (по умолчанию используется вес в 1 тонну)
     * 
     * @return array|bool - вернёт 'true' в случае успешного сохранения и 'false' в случае ошибки
     */
    public function savePriceBuy($productName, $price, $weight = '1 тонна')
    {
        echo 'Выполнено сохранение цены скупки товара «'.htmlspecialchars($productName).'» ('.number_format($price, 0, ',', ' ').' руб., '.htmlspecialchars($weight).'). Скупщик: '.htmlspecialchars($this->name).'<br>';
        return true;
    }

    /**
     * Скачивание цен товаров. Метод получает цены закупки и сохраняет их у себя в базе данных
     * @return int - вернёт количество товаров, для которых было выполнено сохранение цен
     */
    public function downloadPrices()
    {
        $countProductsSaved = 0;

        $contentWithProducts = $this->getContentWithProducts();
        $products = $this->parseProducts($contentWithProducts);
        foreach ($products as $product) {
            $saveResult = $this->savePriceBuy($product['name'], $product['price'], $product['weight']);
            if ($saveResult == true) {
                $countProductsSaved++;
            }
        }

        return $countProductsSaved;
    }
}

?>

products_buyer_1.class.php

Представляет класс первого скупщика ProductsBuyer_1, у которого цены товаров публикуются на сайте. Класс содержит определения следующих методов:
  • getContentWithProducts() – подключается к сайту скупщика, получает и возвращает HTML-код со списком товаров и цен;
  • parseProducts($contentWithProducts) – ищет в исходной строке товары и их цены, затем объединяет полученные данные в итоговый стандартизированный массив.
<?
//Подключаем абстрактный класс скупщика товара
require_once(__DIR__.'/abstract_products_buyer.class.php');

/**
 * Товарный скупщик 1
 */
class ProductsBuyer_1 extends AbstractProductsBuyer
{
    protected $name = 'ООО «Скупщик-1»';

    /**
     * Подключается к сайту скупщика и получает HTML-код со списком товаров и цен
     * @return string - возвращает строку с HTML-кодом, содержащий список товаров и цен
     */
    public function getContentWithProducts()
    {
        return '
        <html>
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        </head>
        <body>
            Цены на зерно
            <table id="id_products_list">
            <tr>
                <td>Культура</td>
                <td>Мера</td>
                <td>Цена</td>
            </tr>
            <tr>
                <td>Пшеница</td>
                <td>Тонна</td>
                <td>15000</td>
            </tr>
            <tr>
                <td>Рожь</td>
                <td>Тонна</td>
                <td>8600</td>
            </tr>
            <tr>
                <td>Ячмень</td>
                <td>Тонна</td>
                <td>12350</td>
            </tr>
            <tr>
                <td>Кукуруза</td>
                <td>Тонна</td>
                <td>8800</td>
            </tr>
            <tr>
                <td>Горох</td>
                <td>Тонна</td>
                <td>16000</td>
            </tr>
            <table>
        </body>
        </html>
        ';
    }

    /**
     * Ищет в исходной строке товары и их цены, затем объединяет полученные данные в итоговый стандартизированный массив
     * @param string $contentWithProducts - строка с HTML-кодом списка товаров и цен
     * @return array - вернёт массив товаров
     */
    public function parseProducts($contentWithProducts)
    {
        //Включаем игнорирование ошибок структуры HTML при разборе структуры через DOMDocument
        libxml_use_internal_errors(true);

        $dom = new DOMDocument();
        $dom->loadHTML($contentWithProducts);
        $xpath = new DOMXPath($dom);

        //Получаем товарные строки в таблице с id="id_products_list"
        $returnProducts = array();
        $productsRows = $xpath->query('//table[@id="id_products_list"]/tr');
        if ($productsRows->length > 1) {
            //Обход начинаем со второй строки (i=1, т.к в первой строке находится заголовок таблицы)
            for ($i = 1; $i < $productsRows->length; $i++) {
                $props = $productsRows->item($i)->getElementsByTagName('td');
                $returnProducts[] = array(
                    'name' => trim($props->item(0)->nodeValue),
                    'weight' => trim($props->item(1)->nodeValue),
                    'price'   => (int)($props->item(2)->nodeValue),
                );
            }
        }

        return $returnProducts;
    }
}

?>

products_buyer_2.class.php

Представляет класс второго скупщика ProductsBuyer_2, у которого есть собственное API по отдаче цен. Класс содержит определения следующих методов:
  • getContentWithProducts() – подключается к API скупщика и получает массив товаров;
  • parseProducts($contentWithProducts) – обходит сырой массив с товарами скупщика и создаёт итоговый стандартизированный массив.
<?
//Подключаем абстрактный класс скупщика товара
require_once(__DIR__.'/abstract_products_buyer.class.php');

/**
 * Товарный скупщик 2
 */
class ProductsBuyer_2 extends AbstractProductsBuyer
{
    protected $name = 'АО «Скупщик-2»';

    /**
     * Подключается к API сайта скупщика и получает массив товаров
     * @return array - возвращает массив с товаров
     */
    public function getContentWithProducts()
    {
        $jsonContent = '{"products":{"1":{"NAME":"Пшеница","MEASURE":"1 тонна","COST":"15500","DATE":"2020-02-01"},"2":{"NAME":"Рожь","MEASURE":"1 тонна","COST":"9200","DATE":"2020-02-01"},"3":{"NAME":"Ячмень","MEASURE":"1 тонна","COST":"11440","DATE":"2020-02-01"},"4":{"NAME":"Кукуруза","MEASURE":"1 тонна","COST":"9100","DATE":"2020-02-01"},"5":{"NAME":"Горох","MEASURE":"1 тонна","COST":"15100","DATE":"2020-02-01"}}}';
        $arProducts = json_decode($jsonContent, true);

        return $arProducts;
    }

    /**
     * Обходит сырой массив с товарами скупщика и создаёт итоговый стандартизированный массив
     * @param string $arProducts - сырой массив с товарами скупщика
     * @return array - вернёт массив товаров
     */
    public function parseProducts($arProducts)
    {
        $returnProducts = array();

        foreach ($arProducts['products'] as $product) {
            //Приводим вес к нормальному значению
            $weight = $product['MEASURE'];
            if ($weight == '1 тонна') {
                $weight = 'Тонна';
            }

            $returnProducts[] = array(
                'name' => $product['NAME'],
                'weight' => $weight,
                'price'   => (int)$product['COST'],
            );
        }

        return $returnProducts;
    }
}

?>

Как можно заметить, классы ProductsBuyer_1 и ProductsBuyer_2 работают с разными исходными данным – в первом случае это сайт, а во втором API. Но благодаря тому, что методы getContentWithProducts() и parseProducts($contentWithProducts) реализуются для каждого класса по-своему, адаптируясь под исходные данные, то работа шаблонного метода downloadPrices() не нарушается. 

test.php

Содержит тестовый скрипт, в котором создаётся первый скупщик $buyer_1 (объект класса ProductsBuyer_1) и загружаются его цены:
//Создаём скупщика 1 и скачиваем его цены
echo 'СКУПЩИК 1<br>';
echo '-----------------<br>';
$buyer_1 = new ProductsBuyer_1();
$buyer_1->downloadPrices();

Затем создаётся второй скупщик $buyer_2 (объект класса ProductsBuyer_2) и происходит загрузка его цен:
//Создаём скупщика 2 и скачиваем его цены
echo '<br>СКУПЩИК 2<br>';
echo '-----------------<br>';
$buyer_2 = new ProductsBuyer_2();
$buyer_2->downloadPrices();

Полный код скрипта test.php
<?
//Подключаем класс товарного скупщика №1
require_once(__DIR__.'/products_buyer_1.class.php');

//Подключаем класс товарного скупщика №2
require_once(__DIR__.'/products_buyer_2.class.php');

//Создаём скупщика 1 и скачиваем его цены
echo 'СКУПЩИК 1<br>';
echo '-----------------<br>';
$buyer_1 = new ProductsBuyer_1();
$buyer_1->downloadPrices();


//Создаём скупщика 2 и скачиваем его цены
echo '<br>СКУПЩИК 2<br>';
echo '-----------------<br>';
$buyer_2 = new ProductsBuyer_2();
$buyer_2->downloadPrices();

?>

В результате выполнения скрипта test.php на экран будут выведены два списка с сообщениями о сохранении цен от Скупщика 1 и Скупщика 2
СКУПЩИК 1
-----------------
Выполнено сохранение цены скупки товара «Пшеница» (15 000 руб., Тонна). Скупщик: ООО «Скупщик-1»
Выполнено сохранение цены скупки товара «Рожь» (8 600 руб., Тонна). Скупщик: ООО «Скупщик-1»
Выполнено сохранение цены скупки товара «Ячмень» (12 350 руб., Тонна). Скупщик: ООО «Скупщик-1»
Выполнено сохранение цены скупки товара «Кукуруза» (8 800 руб., Тонна). Скупщик: ООО «Скупщик-1»
Выполнено сохранение цены скупки товара «Горох» (16 000 руб., Тонна). Скупщик: ООО «Скупщик-1»

СКУПЩИК 2
-----------------
Выполнено сохранение цены скупки товара «Пшеница» (15 500 руб., Тонна). Скупщик: АО «Скупщик-2»
Выполнено сохранение цены скупки товара «Рожь» (9 200 руб., Тонна). Скупщик: АО «Скупщик-2»
Выполнено сохранение цены скупки товара «Ячмень» (11 440 руб., Тонна). Скупщик: АО «Скупщик-2»
Выполнено сохранение цены скупки товара «Кукуруза» (9 100 руб., Тонна). Скупщик: АО «Скупщик-2»
Выполнено сохранение цены скупки товара «Горох» (15 100 руб., Тонна). Скупщик: АО «Скупщик-2»