3.4. Decorator

3.4.1. Intent

According to the Gang of Four, the Decorator pattern is a way to “attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.” (Design Patterns: Elements of Reusable Object-Oriented Software, 2013, p. 175).

Why would you want to find an alternative to subclassing ? Because subclassing is a static mechanism. This isn’t flexible and you won’t be able to control how and when to add functionality to your object.

To illustrate it, I have decided to act like a fast food restaurant that sells submarine sandwiches (who said Subway?). First, choose a bread type (italian bread, honey-oat bred, etc.) then “decorate” it with some toppings (like bacon, cheddar, etc.) and finally get the price and the description for your combination.

3.4.2. When to use it?

It’s better to use the Decorator pattern when a large number of combinations are possible when developing your application or a feature. In that case, subclassing is not appropriate because it will simply require you to write lots of classes to support all the possible combinations.

3.4.3. Diagram

Created using PhpStorm and yFiles.

Class diagram of the Decorator pattern

3.4.4. Implementation

BreadInterface.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php

namespace Phpatterns\Structural\Decorator;

interface BreadInterface
{
    /**
     * Retrieve the description for a given Bread
     * @return string
     */
    public function getDescription();

    /**
     * Retrieve the price for a given Bread
     * @return float
     */
    public function getPrice();
}

HoneyOatBread.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php

namespace Phpatterns\Structural\Decorator\Bread;

use Phpatterns\Structural\Decorator;

class HoneyOatBread implements Decorator\BreadInterface
{
    /** @var string */
    private $description;

    /** @var float */
    private $price;

    public function __construct()
    {
        $this->description = "Honey Oat bread";
        $this->price = 1.99;
    }

    /**
     * Retrieve the description for HoneyOatBread
     * @return string
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * Retrieve the price for HoneyOatBread
     * @return float
     */
    public function getPrice()
    {
        return $this->price;
    }
}

ItalianBread.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php

namespace Phpatterns\Structural\Decorator\Bread;

use Phpatterns\Structural\Decorator;

class ItalianBread implements Decorator\BreadInterface
{
    /** @var string */
    private $description;

    /** @var float */
    private $price;

    public function __construct()
    {
        $this->description = "Italian bread";
        $this->price = 1.0;
    }

    /**
     * Retrieve the description for ItalianBread
     * @return string
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * Retrieve the price for ItalianBread
     * @return float
     */
    public function getPrice()
    {
        return $this->price;
    }
}

AbstractToppingDecorator.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php

namespace Phpatterns\Structural\Decorator;

abstract class AbstractToppingDecorator implements BreadInterface
{
    /** @var string */
    protected $description;

    /** @var float */
    protected $price;

    /**
     * @var BreadInterface
     * The element that will be wrapped (Bread or Topping)
     */
    protected $decoratedElement;

    public function __construct(BreadInterface $element)
    {
        $this->decoratedElement = $element;
    }

    /**
     * Retrieve the description for wrapped elements
     * @return string
     */
    abstract public function getDescription();

    /**
     * Retrieve the price for wrapped elements
     * @return float
     */
    abstract public function getPrice();
}

Bacon.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

namespace Phpatterns\Structural\Decorator\Topping;

use Phpatterns\Structural\Decorator;

class Bacon extends Decorator\AbstractToppingDecorator
{
    public function __construct(Decorator\BreadInterface $element)
    {
        parent::__construct($element);
        $this->description = "Bacon";
        $this->price = 0.59;
    }

    /**
     * Retrieve the description for the Bacon + the wrapped element
     * @return string
     */
    public function getDescription()
    {
        return sprintf(
            "%s, %s",
            $this->decoratedElement->getDescription(),
            $this->description
        );
    }

    /**
     * Retrieve the price for the Bacon + the wrapped element
     * @return float
     */
    public function getPrice()
    {
        return $this->price + $this->decoratedElement->getPrice();
    }
}

Cheddar.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

namespace Phpatterns\Structural\Decorator\Topping;

use Phpatterns\Structural\Decorator;

class Cheddar extends Decorator\AbstractToppingDecorator
{
    public function __construct(Decorator\BreadInterface $element)
    {
        parent::__construct($element);
        $this->description = "Cheddar";
        $this->price = 0.79;
    }

    /**
     * Retrieve the description for the Cheddar + the wrapped element
     * @return string
     */
    public function getDescription()
    {
        return sprintf(
            "%s, %s",
            $this->decoratedElement->getDescription(),
            $this->description
        );
    }

    /**
     * Retrieve the price for the Cheddar + the wrapped element
     * @return float
     */
    public function getPrice()
    {
        return $this->price + $this->decoratedElement->getPrice();
    }
}

3.4.5. Tests

DecoratorTest.php

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
<?php

namespace Test\Phpatterns\Structural\Decorator;

use Phpatterns\Structural\Decorator;
use Phpatterns\Structural\Decorator\Bread;
use Phpatterns\Structural\Decorator\Topping;

class DecoratorTest extends \PHPUnit_Framework_TestCase
{
    /**
     * Decorator pattern requirement: the decorator must implement BreadInterface interface
     */
    public function testToppingDecoratorImplementsBreadInterface()
    {
        $this->assertTrue(
            is_subclass_of(
                Decorator\AbstractToppingDecorator::class,
                Decorator\BreadInterface::class
            )
        );
    }

    /**
     * Decorator pattern requirement: the decorator must be type hinted
     * (with BreadInterface interface in our case)
     *
     */
    public function testToppingDecoratorConstructor()
    {
        /*
         * In PHP 7, a TypeError exception is thrown when arguments' types
         * passed to a function do not match their corresponding declared
         * parameters.
         * see http://php.net/manual/en/class.typeerror.php
         */
        if (version_compare(PHP_VERSION, '7', '>=')) {
            $this->setExpectedException('TypeError');
        } else {
            $this->setExpectedException('PHPUnit_Framework_Error');
        }

        $this->getMockForAbstractClass(
            Decorator\AbstractToppingDecorator::class,
            [new \stdClass()]
        );
    }

    /**
     * Testing the decorator mechanism + descriptions and prices
     * @param Decorator\BreadInterface $bread
     * @param Decorator\AbstractToppingDecorator $decorators
     * @param string $description
     * @param float $price
     * @dataProvider decoratorProvider
     */
    public function testDecoratorMechanism($bread, $decorators, $description, $price)
    {
        /** @var $element Decorator\BreadInterface */
        $element = new $bread();

        foreach ($decorators as $decorator) {
            $element = new $decorator($element);
        }

        $this->assertSame($description, $element->getDescription());
        $this->assertSame($price, $element->getPrice());

    }

    public function decoratorProvider()
    {
        return [
            [
                Bread\ItalianBread::class,
                [Topping\Cheddar::class],
                'Italian bread, Cheddar',
                1.79
            ],
            [
                Bread\HoneyOatBread::class,
                [Topping\Bacon::class],
                'Honey Oat bread, Bacon',
                2.58
            ],
            [
                Bread\ItalianBread::class,
                [Topping\Cheddar::class, Topping\Cheddar::class, Topping\Bacon::class],
                'Italian bread, Cheddar, Cheddar, Bacon',
                3.17
            ],
            [
                Bread\HoneyOatBread::class,
                [],
                'Honey Oat bread',
                1.99
            ]
        ];
    }
}