Шаблонный метод (Template Method) — шаблон проектирования
6 февраля 2020
Шаблон предлагает схему, в которой общий порядок работы объекта и каркас алгоритма задаётся в базовом классе, а в дочерних классах объект и алгоритм получают законченную форму, при этом общая логика, намеченная в базовом классе, не нарушается.
Под алгоритмом тут понимается выполнение какого-то действия с помощью метода, определённого в базовом классе и который является окончательным (в PHP может быть помечен ключевым словом final). В терминологии шаблона данный окончательный метод и есть шаблонный метод. Дочерние же классы реализуют другие методы, используемые шаблонным методом и тем самым в дочернем классе объект получает собственную реализацию алгоритма.
Код реализации паттерна на языке PHP. Скачать исходники (ZIP, 4 Кб)
Шаблон состоит из двух элементов:
Теперь перейдём к коду.
Здесь определён шаблонный метод downloadPrices. Внутри себя он вызывает методы дочерних классов getContentWithProducts() и parseProducts($contentWithProducts), поэтому несмотря на то, что код шаблонного метода для всех подклассов одинаковый, у каждого дочернего класса будет собственная реализация используемых им методов.
Как можно заметить, классы ProductsBuyer_1 и ProductsBuyer_2 работают с разными исходными данным – в первом случае это сайт, а во втором API. Но благодаря тому, что методы getContentWithProducts() и parseProducts($contentWithProducts) реализуются для каждого класса по-своему, адаптируясь под исходные данные, то работа шаблонного метода downloadPrices() не нарушается.
Затем создаётся второй скупщик $buyer_2 (объект класса ProductsBuyer_2) и происходит загрузка его цен:
Полный код скрипта test.php
В результате выполнения скрипта test.php на экран будут выведены два списка с сообщениями о сохранении цен от Скупщика 1 и Скупщика 2
Под алгоритмом тут понимается выполнение какого-то действия с помощью метода, определённого в базовом классе и который является окончательным (в 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»