Декоратор (Decorator) — шаблон проектирования

23 января 2020
Шаблон предлагает схему для замены исходного объекта новым, который дополняет исходный объект новым функционалом или изменяет его. 

Пример задачи: в расчёт стоимости доставки груза добавить учёт скидок по бонусной программе.

Смысл паттерна сводится к следующему. У нас есть класс груза 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