Декоратор (Decorator) — шаблон проектирования
23 января 2020
Шаблон предлагает схему для замены исходного объекта новым, который дополняет исходный объект новым функционалом или изменяет его.
Смысл паттерна сводится к следующему. У нас есть класс груза Cargo и нам нужно его метод по получению стоимости доставки груза getDeliveryCost() дополнить функционалом, который при подсчёте цены учитывал бы бонусные баллы, дающие скидку.
Теоретически, мы можем изменить класс Cargo, а можем создать дочерний класс CargoV2 и в нём переопределить или же создать новый метод getDeliveryCostWithBonuses(). Но что, если изменять класс Cargo нельзя? Допустим, он входит в официальный SDK некоторого сервиса доставки и правки могут затереться при очередном обновлении библиотеки. Сделать дочерний класс можно, но тогда придётся во многих местах кода менять Cargo на обновлённый класс CargoV2.
Есть третий вариант — сделать класс-декоратор CargoDecoratorWithBonuses. В нём будет находиться одно свойство-ссылка на объект класса Cargo и один метод getDeliveryCost(), в котором цена доставки рассчитывается на основе объекта класса Cargo, но с учётом бонусов. Данный класс-декоратор используется только при подсчёте суммы доставки груза. Тем самым, по задумке при реализации задачи правки будут вноситься исключительно в клиентском коде и только в местах расчета цены. Этим самым и удобен шаблон Декоратор.
Перейдём к файлам и примеру в коде.
Ниже приводится реализация паттерна на языке PHP. Скачать исходники (ZIP, 2 Кб)
Класс содержит следующие элементы:
Результат работы скрипта test.php
Пример задачи: в расчёт стоимости доставки груза добавить учёт скидок по бонусной программе.
Смысл паттерна сводится к следующему. У нас есть класс груза Cargo и нам нужно его метод по получению стоимости доставки груза getDeliveryCost() дополнить функционалом, который при подсчёте цены учитывал бы бонусные баллы, дающие скидку.
Теоретически, мы можем изменить класс Cargo, а можем создать дочерний класс CargoV2 и в нём переопределить или же создать новый метод getDeliveryCostWithBonuses(). Но что, если изменять класс Cargo нельзя? Допустим, он входит в официальный SDK некоторого сервиса доставки и правки могут затереться при очередном обновлении библиотеки. Сделать дочерний класс можно, но тогда придётся во многих местах кода менять Cargo на обновлённый класс CargoV2.
Есть третий вариант — сделать класс-декоратор CargoDecoratorWithBonuses. В нём будет находиться одно свойство-ссылка на объект класса Cargo и один метод getDeliveryCost(), в котором цена доставки рассчитывается на основе объекта класса Cargo, но с учётом бонусов. Данный класс-декоратор используется только при подсчёте суммы доставки груза. Тем самым, по задумке при реализации задачи правки будут вноситься исключительно в клиентском коде и только в местах расчета цены. Этим самым и удобен шаблон Декоратор.
Перейдём к файлам и примеру в коде.
Ниже приводится реализация паттерна на языке PHP. Скачать исходники (ZIP, 2 Кб)

cargo.class.php
Cодержит класс Cargo, включающий в себя:- Свойство id — хранит идентификтор груза;
- Конструктор __construct($id) — при создании объекта записывает идентификатор груза в свойство id;
- Метод getDeliveryCost() — считает и возвращает стоимость доставки груза.
<?
/**
* Класс груза
*/
class Cargo
{
/**
* Идентификатор груза
* @var int
*/
private $id;
/**
* Конструктор объекта
* @param int id - идентификатор груза
*/
public function __construct($id)
{
$this->id = $id;
}
/**
* Возвращает стоимость доставки груза
* @return int
*/
public function getDeliveryCost()
{
/*
Представим, что тут находится обработка по получению товаров груза,
анализ их объёма, веса, подсчёт доставки каждого товара, затем суммирование
и получение итогового значения
*/
$sumCost = 4840;
return $sumCost;
}
}
?>
cargo_decorator_with_bonuses.class.php
Представляет класс-декоратор CargoDecoratorWithBonuses, который при расчёте стоимости доставки учитывает участие владельца груза в бонусной программе и предоставляет скидки.Класс содержит следующие элементы:
- Свойство cargo — хранит ссылку на объект класса Cargo.
- Конструктор __construct($cargo) — при создании объекта записывает объект груза $cargo в свойство cargo;
- Метод getDeliveryCost() — считает и возвращает стоимость доставки груза с учётом скидок по бонусной программе. Обратите внимание, что для получения исходной стоимости доставки (до скидок) вызывается метод GetDeliveryCost() объекта из свойства cargo.
<?
/**
* Класс-декоратор, который рассчитывает стоимость доставки груза с учётом бонусной программы
*/
class CargoDecoratorWithBonuses
{
/**
* Объект груза
* @var Cargo
*/
private $cargo;
/**
* Конструктор объекта
* @param Cargo cargo - объект груза
*/
public function __construct($cargo)
{
$this->cargo = $cargo;
}
/**
* Возвращает стоимость доставки груза с учётом бонусов владельца груза
* @return int
*/
public function getDeliveryCost()
{
//Исходная стоимость доставки
$sumCostOriginal = $this->cargo->GetDeliveryCost();
/*
Представим, что тут находится обработка по получению владельца груза, анализу
его участия в бонусной программе. Допустим, владелец участвует в бонусной программе и
накопил определённые баллы, которые дают ему 10% скидки на доставку.
*/
$clientBonusDiscountPercent = 10;
if ($clientBonusDiscountPercent > 0) {
$sumCostWithBonuses = $sumCostOriginal - ceil($sumCostOriginal * $clientBonusDiscountPercent * 0.01);
} else {
$sumCostWithBonuses = $sumCostOriginal;
}
return $sumCostWithBonuses;
}
}
?>
test.php
Тестовый скрипт, в котором создаётся объект груза $cargo, затем на его основе создаётся объект-декоратор $cargoWithBonuses. В конце выводится обычная цена доставки и цена с учётом действия бонусной программы.<?
//Класс груза
require_once(__DIR__.'/cargo.class.php');
//Класс-декоратор груза, который в стоимости доставки учитывает бонусную программу
require_once(__DIR__.'/cargo_decorator_with_bonuses.class.php');
//Создаём объект груза
$cargoId = 17859;
$cargo = new Cargo($cargoId);
//Создаём объект-декоратор груза, учитывающий бонусную программу
$cargoWithBonuses = new CargoDecoratorWithBonuses($cargo);
echo 'Стоимость доставки груза: '.($cargo->getDeliveryCost()).'<br>';
echo 'Стоимость доставки груза с учётом скидок: '.($cargoWithBonuses->getDeliveryCost());
?>
Результат работы скрипта test.php
Стоимость доставки груза: 4840
Стоимость доставки груза с учётом скидок: 4356