decorator Декоратор (англ. Decorator) относиться к классу структурных шаблонов. Он используется для динамического расширения функциональности объекта. Является гибкой альтернативой наследованию.

Смысл заключается в том, чтобы можно было безболезненно комбинировать различные декораторы в произвольном порядке, навешивая их на различные объекты. В некотором роде, это похоже на технологию traits, за исключением того, что декораторы динамически навешиваются на объект, а traits статически на класс.

На примере мы имеем класс Cheeseburger, который реализует интерфейс BurgerInterface. Тут мы замечаем, что в классе Cheeseburger не хватает сыра. Это основной ингредиент чизбургера и хорошо бы нам дописать его в класс. Но давайте представим, что переписывать класс мы не можем. “Давайте наследуем Cheeseburger и расширим его”, скажете вы. А если нам нужно ещё что-то добавить к Cheeseburger? Опять наследование. А если нужно это всё объединить и ещё что-то добавить? Опять наследование. Потом опять и опять…

Декоратор помогает нам сделать это всё более гибко.

Так как наш абстрактный декоратор AbstractDecorator реализует интерфейс BurgerInterface, мы можем расширять его на уровне абстракции.

Добавим сыр средствами CheeseburgerCheeseDecorator.

Наш декоратор (наследник AbstractDecorator), в конструктор ждёт объект типа BurgerInterface. Как упоминалось выше, AbstractDecorator реализует интерфейс BurgerInterface. Но метод BurgerInterface->makeBurger() он не реализует, а помечает как абстрактный. Это значит что наследники должны реализовать его. Вот в нём мы будем декорировать наш чизбургер.

А теперь давайте поменяем соус в чизбургере.

Создаем новый декоратор CheeseburgerSauceDecorator (наследник AbstractDecorator). Теперь мы можем расширять любой объект типа BurgerInterface. Это может быть как исходный чизбургер без сыра(объект Cheeseburger->makeBurger()), так и уже декорированный с сыром (CheeseburgerCheeseDecorator->makeBurger()).

  • Burgers
    • BurgerInterface.php
                    
      <?php
      
      namespace DesignPatterns\Structural\Decorator\Burgers;
      
      /**
       * Interface BurgerInterface
       * @package DesignPatterns\Structural\Decorator\Burgers
       */
      interface BurgerInterface
      {
          /**
           * @return array
           */
          public function makeBurger(): array;
      }
      
                    
    • Cheeseburger.php
                    
      <?php
      
      namespace DesignPatterns\Structural\Decorator\Burgers;
      
      /**
       * Class Cheeseburger
       * @package DesignPatterns\Structural\Decorator\Burgers
       */
      class Cheeseburger implements BurgerInterface
      {
          /**
           * @var string
           */
          protected $meat = 'chicken';
      
          /**
           * @var string
           */
          protected $sauce = 'mayonnaise';
      
          /**
           * @var string
           */
          protected $muffin = 'muffin';
      
          /**
           * @return array
           */
          public function makeBurger(): array
          {
              return [
                  'muffin' => $this->muffin,
                  'sauce'  => $this->sauce,
                  'meat'   => $this->meat,
              ];
          }
      }
      
                    
  • AbstractDecorator.php
                  
    <?php
    
    namespace DesignPatterns\Structural\Decorator;
    
    use DesignPatterns\Structural\Decorator\Burgers\BurgerInterface;
    
    /**
     * Class AbstractDecorator
     * @package DesignPatterns\Structural\Decorator
     */
    abstract class AbstractDecorator implements BurgerInterface
    {
    
        /**
         * @var BurgerInterface
         */
        protected $burger;
    
        /**
         * AbstractDecorator constructor.
         *
         * @param BurgerInterface $burger
         */
        public function __construct(BurgerInterface $burger)
        {
            $this->burger = $burger;
        }
    
        /**
         * @return array
         */
        abstract public function makeBurger(): array;
    }
    
                  
  • CheeseburgerCheeseDecorator.php
                  
    <?php
    
    namespace DesignPatterns\Structural\Decorator;
    
    /**
     * Class CheeseburgerCheeseDecorator
     * @package DesignPatterns\Structural\Decorator
     */
    class CheeseburgerCheeseDecorator extends AbstractDecorator
    {
        const BEST_CHEESE_FOR_BURGER = 'Cheddar';
    
        /**
         * @return array
         */
        public function makeBurger(): array
        {
            $burger = $this->burger->makeBurger();
    
            return array_replace(
                $burger,
                [
                    'cheese' => self::BEST_CHEESE_FOR_BURGER,
                ]
            );
        }
    }
    
                  
  • CheeseburgerSauceDecorator.php
                  
    <?php
    
    namespace DesignPatterns\Structural\Decorator;
    
    /**
     * Class CheeseburgerSauceDecorator
     * @package DesignPatterns\Structural\Decorator
     */
    class CheeseburgerSauceDecorator extends AbstractDecorator
    {
        const NEW_SAUCE = 'Ukrainian sauce';
    
        /**
         * @return array
         */
        public function makeBurger(): array
        {
            $burger = $this->burger->makeBurger();
    
            return array_replace(
                $burger,
                [
                    'sauce' => self::NEW_SAUCE,
                ]
            );
        }
    }
    
                  
  • Test case:

    DecoratorTest.php
      
    <?php
    namespace DesignPatterns\Tests\Structural\Decorator;
    
    use DesignPatterns\Structural\Decorator\Burgers\Cheeseburger;
    use DesignPatterns\Structural\Decorator\CheeseburgerCheeseDecorator;
    use DesignPatterns\Structural\Decorator\CheeseburgerSauceDecorator;
    
    class DecoratorTest extends \PHPUnit_Framework_TestCase
    {
        public function testAddCheeseDecorator()
        {
            $cheeseburger = new Cheeseburger();
            $cheeseDecorator = new CheeseburgerCheeseDecorator($cheeseburger);
    
            $this->assertArrayNotHasKey('cheese', $cheeseburger->makeBurger());
            $this->assertArrayHasKey('cheese', $cheeseDecorator->makeBurger());
        }
    
        public function testAddSauceDecorator()
        {
            $cheeseburger = new Cheeseburger();
            $sauceDecorator = new CheeseburgerSauceDecorator($cheeseburger);
    
            $cheeseburger = $cheeseburger->makeBurger();
            $decorateCheeseburger = $sauceDecorator->makeBurger();
    
            $this->assertArrayHasKey('sauce', $cheeseburger);
            $this->assertArrayHasKey('sauce', $decorateCheeseburger);
    
            $this->assertEquals('mayonnaise', $cheeseburger['sauce']);
            $this->assertNotEquals('mayonnaise', $decorateCheeseburger['sauce']);
            $this->assertEquals($sauceDecorator::NEW_SAUCE, $decorateCheeseburger['sauce']);
        }
    
        public function testAddSauceAndCheeseDecorator()
        {
            $cheeseburger = new Cheeseburger();
            $cheeseDecorator = new CheeseburgerCheeseDecorator($cheeseburger);
            $sauceDecorator = new CheeseburgerSauceDecorator($cheeseDecorator);
    
            $decorateCheeseburger = $sauceDecorator->makeBurger();
    
            $this->assertArrayHasKey('cheese', $decorateCheeseburger);
    
            $this->assertArrayHasKey('cheese', $decorateCheeseburger);
            $this->assertEquals($sauceDecorator::NEW_SAUCE, $decorateCheeseburger['sauce']);
        }
    }