Используем паттерн Decorator в Bitrix

Применять паттерны программирования — хорошая практика. К сожалению, на проектах CMS Bitrix редко встречаются примеры использования. 

В статье я покажу на примере, как можно использовать паттерн Декоратор.

А также рассмотрим этот паттерн в целом: его распространенные реализации в PHP, возможные альтернативы и ситуации, в которых лучше избегать его использования.

f4c3ef200af62f488fd3128ae7e78ebd.jpg

Шаблон Декоратор позволяет добавлять новую функциональность к объекту, оборачивая его в другие объекты-декораторы. Эти декораторы имеют тот же интерфейс, что и исходный объект, и добавляют новую функциональность, выполняя дополнительные операции перед или после вызовов методов исходного объекта. Таким образом, можно создать цепочку декораторов, каждый из которых добавляет новый функционал к исходному объекту.

Реализация шаблона Декоратор в PHP обычно включает следующие элементы:

  1. Интерфейс Компонента: определяет интерфейс, который должны реализовать все компоненты и их декораторы.

  2. Конкретный компонент: представляет основной объект, к которому будут добавляться новые функции. Реализует интерфейс Компонента.

  3. Базовый декоратор: представляет базовый класс для всех декораторов. Реализует интерфейс Компонента, но также содержит ссылку на объект типа Компонента, к которому будет добавлена новая функциональность.

  4. Конкретные декораторы: расширяют функциональность базового декоратора, добавляя новые функции. Каждый конкретный декоратор имеет ссылку на объект типа Компонента и может выполнять дополнительную логику перед или после вызова методов объекта.

Плюсы и минусы

Преимущества использования шаблона Декоратор:

  • Позволяет добавлять новую функциональность к объектам без изменения их кода.

  • Позволяет добавлять функции как перед, так и после вызова методов.

  • Гибкость в добавлении и комбинировании функциональных возможностей.

Несмотря на множество преимуществ, существуют ситуации, в которых лучше избегать использования шаблона Декоратор:

  • Когда вам нужно создавать большое количество вложенных декораторов, что может привести к сложному коду и снижению производительности.

  • Когда требуется изменять сам класс компонента (класс, к которому добавляется функциональность), вместо добавления новых функций через декораторы.

Что даёт паттерн Декоратор в Bitrix

  1. Расширение функциональности: паттерн Декоратор позволяет добавлять новую функциональность к объектам без изменения их основной структуры. В Bitrix это может быть полезно, например, для добавления дополнительных возможностей к пользователям, элементам инфоблоков и другим сущностям.

  2. Разделение ответственностей: использование декоратора позволяет выделить отдельные функции в отдельные классы, что позволяет лучше структурировать код и улучшить его понятность и поддерживаемость.

  3. Гибкость и масштабируемость: паттерн Декоратор позволяет добавлять и комбинировать декораторы в любом порядке в зависимости от нужд проекта. Это позволяет быстро и гибко изменять функциональность без необходимости полностью переписывать код.

Пример использования

Предположим, мы реализуем кастомный компонент «Корзина». И нам надо вывести стоимость корзины до применения скидки и предварительный расчет стоимости корзины с уже применяемыми скидками.

Реализуем класс Корзинас использованием паттерна Декоратор.

  1. Создадим Interface BasketInterface, который будет определять основные методы для работы с корзиной:

interface BasketInterface {
   public function getTotalPrice();
   public function addItem(\Bitrix\Sale\BasketItem $item);
   public function removeItem(\Bitrix\Sale\BasketItem $item);
}
  1. Создадим базовый класс BaseBasket, который будет реализовывать интерфейс Корзины:

class BaseBasket implements BasketInterface {
   protected $items = [];

   public function getTotalPrice() {
       $totalPrice = 0;
       foreach ($this->items as $item) {
           $totalPrice += $item->getPrice();
       }
       return $totalPrice;
   }

   public function addItem(\Bitrix\Sale\BasketItem $item) {
       $this->items[] = $item;
   }

   public function removeItem(\Bitrix\Sale\BasketItem $item) {
       foreach ($this->items as $key => $val) {
           if ($val == $item) {
               unset($this->items[$key]);
               break;
           }
       }
   }
}
  1. Создадим декоратор для расчета скидки DiscountBasket,  который будет расширять функциональность базового класса BaseBasket:

class DiscountBasket implements BasketInterface
{

   protected $basket;
   protected $discount;

   public function __construct(BasketInterface $basket, $discount)
   {
       $this->basket = $basket;
       $this->discount = $discount;
   }

   public function getTotalPrice()
   {
       $totalPrice = $this->basket->getTotalPrice();
       $discountPrice = $totalPrice * (1 - ($this->discount / 100));

       return $discountPrice;
   }

   public function addItem(\Bitrix\Sale\BasketItem $item)
   {
       $this->basket->addItem($item);
   }

   public function removeItem(\Bitrix\Sale\BasketItem $item)
   {
       $this->basket->removeItem($item);
   }

}
  1. Реализуем наш расчет:

$basket =Basket::loadItemsForFUser(Sale\Fuser::getId(), \Bitrix\Main\Context::getCurrent()->getSite());

$basketItems = $basket->getBasketItems();

$baseBasket = new BaseBasket();
$discountBasket = new DiscountBasket($baseBasket, 10); // Установим скидку 10%

foreach ($basketItems as $index => $basketItem) {
   $discountBasket->addItem($basketItem);
}

$totalPrice = $discountBasket->getTotalPrice(); // Расчет с учетом скидки


?>

Цена без скидки: getBasePrice()); ?>

Новая цена:

Результат вывода:

9b0955a32e69866248621e39bdfa0e9e.jpg

В данном примере, класс BaseBasket реализует основную функциональность корзины — добавление и удаление товаров, а также расчет общей стоимости. 

Класс DiscountBasket является декоратором и расширяет функциональность базовой корзины, добавляя возможность установки скидки на общую стоимость. Этот класс также реализует методы интерфейса BasketInterface и использует базовую корзину для выполнения основных операций.

При создании экземпляра класса DiscountBasket можно передать в конструктор экземпляр базовой корзины и значение скидки. 

Таким образом, методы addItem и removeItem вызываются на объекте DiscountBasket, но фактически выполняют операции на базовой корзине. Метод getTotalPrice также использует функциональность базовой корзины, но применяет скидку к полученной общей стоимости и возвращает результат со скидкой.

Вы можете справедливо заметить, почему нельзя просто сделать наследника от базового класса? Паттерн Декоратор изменяет/добавляет поведение объекта. Вы можете оборачивать объект одним или несколькими декораторами. Эти декораторы отвечают за добавление или изменение функциональности обернутого объекта без изменения его интерфейса. Накладывая несколько декораторов, вы можете постепенно расширять поведение исходного объекта.

Также использование Декоратора возможно для Final классов, которые закрыты для расширения.

  1. Добавим к нашей цене стоимость доставки. Для этого  создадим декоратор, добавляющий стоимость доставки DelliveryBasket:

class DelliveryBasket implements BasketInterface
{

   protected $basket;
   protected $deliveryPrice;

   public function __construct(BasketInterface $basket, $deliveryPrice)
   {
       $this->basket = $basket;
       $this->deliveryPrice = $deliveryPrice;
   }

   public function getTotalPrice()
   {
       $totalPrice = $this->basket->getTotalPrice();
       $discountPrice = $totalPrice + $this->deliveryPrice;

       return $discountPrice;
   }

   public function addItem(\Bitrix\Sale\BasketItem $item)
   {
       $this->basket->addItem($item);
   }

   public function removeItem(\Bitrix\Sale\BasketItem $item)
   {
       $this->basket->removeItem($item);
   }
}
  1. Теперь наша реализация будет выглядеть так:

$basket =Basket::loadItemsForFUser(Sale\Fuser::getId(), \Bitrix\Main\Context::getCurrent()->getSite());

$basketItems = $basket->getBasketItems();

$baseBasket = new BaseBasket();
$discountBasket = new DelliveryBasket( new DiscountBasket($baseBasket, 10), 500); // Установим скидку 10% и добавим доставку

foreach ($basketItems as $index => $basketItem) {
   $discountBasket->addItem($basketItem);
}

$totalPrice = $discountBasket->getTotalPrice(); // Расчет с учетом скидки


?>

Цена без скидки: getBasePrice()); ?>

Новая цена:

Результат вывода:

e5a876466e77b170fd2de9a9ab647d01.jpg

В текущей реализации метод getTotalPrice использует функциональность базовой корзины, применяет скидку к полученной общей стоимости и возвращает результат со скидкой, затем к полученной стоимости добавляет стоимость доставки.

Декоратор имеет альтернативное название — обёртка. Оно более точно описывает суть паттерна: вы помещаете целевой объект в другой объект-обёртку, который запускает базовое поведение объекта, а затем добавляет к результату что-то своё.

Оба объекта имеют общий интерфейс, поэтому для пользователя нет никакой разницы, с каким объектом работать — чистым или обёрнутым. Вы можете использовать несколько разных обёрток одновременно — результат будет иметь объединённое поведение всех обёрток сразу.

В заключение, шаблон Декоратор в PHP предоставляет гибкую и расширяемую альтернативу подклассам для добавления новой функциональности к объектам. Он соответствует принципу открытости-закрытости и позволяет вносить изменения в функциональность объектов во время выполнения. Однако, необходимо оценить ситуацию и учитывать возможные недостатки при использовании этого паттерна.

© Habrahabr.ru