The blog

Mastering the decorator pattern

Published on November 20, 2022

Pre-reading note: if you are unfamiliar with any of the terms, pause your reading and check their meaning before continuing!

In API Platform, but also in Symfony  — and in a lot of softwares  —  it is often recommended to "decorate" to extend third-party code. In API Platform, we are asked to decorate providers, processors, listeners, resolvers, factories…  For an experienced dev, this is trivial. But if you are new to the term, it is not so obvious.

Before discussing the pattern, we need to understand the reasons for its existence. To do this, we must define the difference (in OOP) between composition and inheritance.

Inheritance #

Inheritance is one of the four major concepts associated with object-oriented programming. It includes abstraction, encapsulation, inheritance and polymorphism. Inheritance allows us to acquire properties and methods from the base class in the derived (or child) class. Derived classes usually have a (logical) relationship with the base class.

Benefits of Inheritance
  • Inheritance allows code reuse, to easily inherit features from the parent class without copying the code.
  • It provides a clear hierarchical structure that allows us to break down a model into a simple, easily digestible structure.
Disadvantages of inheritance
  • Inherited functions are slower than normal functions (without mentioning any specific programming language).
  • All variables and methods of the base class are inherited even if they are not used, this causes an unnecessary surplus.
  • Small changes can affect all derived classes in unexpected ways due to tight coupling.
  • Inheritance has a structure defined at compile time: we cannot change anything in the higher classes when "distributing" public or protected methods and properties to derived classes. The details of the parent class are exposed to the child class and this breaks the fundamental concept of encapsulation (i.e. "hiding", "keeping private" properties and methods, to control their contents and actions).

To make an analogy, if I put Crackers on the table for each person with 1 Cracker per person. Without supervision, after 2 or 3 greedy people, there is no more for the others. Then I put the Crackers in a dispenser (it would be a class) which is in charge of checking that only one is distributed to each person. No one can take one directly. I define an accessor `getCracker`. Once empty however, I can't renew the stock. So I also define an `insertCrackers` mutator.

Is it enough? No! I just allowed adding barbecue Cracker! Tragedy, they are not good! So my mutator will also be responsible for controlling the quality of the inserted Crackers.

<?php

class CrackersBox 
{
    private array $authorizedFlavours = ['bacon', 'cheese', 'onion cream', 'garlic and herbs'];
    protected array $crackers = [];
    
    public function getCracker()
    {
        if (empty($this->crackers)) {
            throw new RuntimeException('empty box!');
        }
        
        return array_shift($this->crackers);
    }
    
    public function insertCrackers(array $crackers) 
    {
        if (!empty($this->crackers)) {
            throw new RuntimeException('Finish what is left first!');
        }
        
        $this->crackers = array_filter($crackers, fn ($cracker) => in_array($cracker, $this->authorizedFlavours));
    }
}


$crackersBox = new CrackersBox();
$crackersBox->insertCrackers(['bacon', 'cheese', 'onion cream', 'garlic and herbs']);

echo $crackersBox->getCracker().PHP_EOL; // bacon
echo $crackersBox->getCracker().PHP_EOL; // cheese, no barbecue.

We have properly protected the access and addition of Crackers!

If an implementation of a class is perfect from an encapsulation point of view, and if changes are applied on this class, no changes will be necessary in the classes that inherit from it. Only in reality, as soon as I extend the Crackers box, my new class will have knowledge of all the public and protected properties and methods.

Let's just say I'm not in the mood for Cheese Crackers today and I want to sort out:

<?php

class CrackersBox 
{
    protected array $authorizedFlavours = ['bacon', 'cheese', 'onion cream', 'garlic and herbs'];
    protected array $crackers = [];
    
    public function getCracker()
    {
        if (empty($this->crackers)) {
            throw new RuntimeException('empty box!');
        }
        
        return array_shift($this->crackers);
    }
    
    public function insertCrackers(array $<meta charset="utf-8">crackers) 
    {
        if (!empty($this->crackers)) {
            throw new RuntimeException('We finish the box first!');
        }
        
        $this->crackers = array_filter($crackers, fn ($cracker) => in_array($cracker, $this->authorizedFlavours));
    }
}

class CrackersBoxSortingCheese extends CrackersBox 
{
    public function getCracker()
    {
        while ('cheese' === $cracker = parent::getCracker()){
            $this->crackers[] = $cracker;
        }
        
        return $cracker;
    }
}


$crackersBox = new CrackersBoxSortingCheese();
$crackersBox->insertCrackers(['bacon', 'cheese', 'onion cream', 'garlic and herbs']);
echo $crackersBox->getCracker().PHP_EOL; // bacon
echo $crackersBox->getCracker().PHP_EOL; // onion cream... Phew, no cheese or barbecue

Let's imagine that the brand that distributes these crackers decides to replace the cheese range with 3 variations: beaufort, comté and goat. When I update my `CrackersBox`, I will have to modify my `CrackersBoxSortingCheese`: the inheritance has broken the encapsulation contract.

Composition #

In the composition, one of the classes has one or more instances of other classes often obtained by passing them to the constructor.

Advantages of the composition
  • Dependencies are less compared to inheritance, the idea being to rely mainly on typed arguments with the help of interfaces to be as versatile and resilient as possible in the future.
  • The objects are defined at runtime, they do not have the possibility to access the protected data of another object: this allows to maintain the encapsulation.
Disadvantages of the composition
  • It requires more time and produces more verbose code.
  • The system is dependent on the interrelation between the objects.
  • A good breakdown to keep flexibility while ensuring the scalability of the code without having to make a deep restructuring is difficult: it is necessary to spend some time upstream to study the possibilities of structures, design pattern, to make the optimal choice.

Why choose composition over inheritance #

Composition is an object-oriented programming concept that stipulates that classes must have polymorphic behavior (we are talking about method overloading here, which is not possible in PHP for example). This makes the whole thing more flexible, easier to maintain despite a larger code at the beginning. This is the reason for the saying "Favor object composition over class inheritance".

Inheritance is a powerful tool. If used well, it can make your code compact, easy to read and fast to develop, but there are many pitfalls a person can easily fall into. Small changes can have disastrous consequences as seen above. That's why, as a rule, we opt for composition instead.

When to inherit or not? #

Let's look at some OOP typing rules and the limits they impose. With these in mind, you will be able to identify situations where inheriting is not a good solution.

Do you know Barbara Liskov? She produced a publication called "A behavioral notion of subtyping" where she formalized the Liskov Substitution Principle, one of the SOLID principles. I encourage you to study these principles (and Demeter's Law).

Description of the Liskov Substitution Principle

If S is a subtype of T, then objects of type T in a program can be replaced by objects of type S without altering any properties of that program.

Liskov's substitution principle imposes certain requirements on method signatures in OOP languages. Among them, two notions that we will detail:

  • Respecting the contravariance of method parameter types in the subtype.
  • Respecting the covariance of the return types of the methods in the subtype.
Covariance and Contravariance

Many programming languages support subtyping. For example, if the type Cat is a subtype of Animal, an element of type Cat should be accepted wherever an element of type Animal is used.

Variance refers to how subtyping between more complex types relates to subtyping between their components. For example, how should a list of cats identify itself as a list of animals? Or how should a function that returns a cat identify as a function that returns an animal? These sentences are complex, and nothing beats a hands-on example to get the hang of the rule.

Example of contravariance in PHP
<?php

interface LivingThingInterface {}
interface AnimalInterface extends LivingThingInterface {}

interface DogInterface extends AnimalInterface {}
interface PlantInterface extends LivingThingInterface {}


class Dog implements DogInterface {}

class Plant implements PlantInterface {}

class Pet
{
	public function __construct(AnimalInterface $animal) {}
}

class PetDog extends Pet
{
 	// Works
	public function __construct(DogInterface $dog)
	{
 		parent::__construct($dog);
	}
}

class PlantPet extends Pet
{
 	// Does not work
	public function __construct(PlantInterface $plant)
	{
 		parent::__construct($plant);
	}
}

DogInterface inherits from AnimalInterface, the inheritance tolerates this change of type, it is the contravariance. S subtype of T -> S being DogInterface and T being AnimalInterface.

PlantInterface inherits from LivingThingInterface, just like Animal. However PlantInterface is not a subtype of AnimalInterface, it is a subtype of LivingThingInterface.

Let's consider it as a cousin. Inheritance will refuse this type change.

An example of covariance in PHP

<?php

class LivingThing{}

class Plant extends LivingThing {}
class Animal extends LivingThing {}

class Cat extends Animal {}

interface AnimalShelter
{
	public function adopt(string $name): Animal;
}

class CatShelter implements AnimalShelter
{
 	// Works
	public function adopt(string $name): Cat
	{
 		return new Cat($name);
	}
}

class PlantShelter implements AnimalShelter
{
 	// Does not work
	public function adopt(string $name): Plant
	{
 		return new Plant($name);
	}
}

Cat inherits from Animal, the inheritance tolerates this change of type, it is the contravariance. S subtype of T -> S being Cat and T being Animal.

Plant inherits from LivingThingInterface, just like Animal. However Plant is not a subtype of Animal. It is a subtype of LivingThing.

Consider it a cousin. Inheritance will refuse this change of type.

Note that by the obviousness of the names used, it seems strange to have classified Plant under Animal, but it is not always so obvious. A better structure would have been to have everything derived from LivingThing.

Also, new exceptions should not be thrown by methods of the subtype, unless they are subtypes of exceptions thrown by methods of the supertype. You are beginning to see the limits of inheritance!

There comes a point in our structures where these relationships conflict with what we want to accomplish. So, rather than inheriting to add behavior, it becomes clearer by creating classes that do just what is desired and injecting them into classes that need that behavior. As a bonus, these classes can be used more easily.

Composition makes a lot more sense, all right. And what about the decoration?

The pattern

Let's say we have used composition in our code. Among the SOLID principles is also the O for Open/Closed Principle. This principle states: that software entities (classes, modules, functions, etc.) should be open to extension, but closed to modification. That is, such an entity can allow its behavior to be extended without changing its source code.

How to add behavior to a class, but without touching its interface and signatures, without adding new methods? It is possible to inherit and overload, but we don't want to fall back into the limitations we saw above. So we compose. We compose by decorating!

A decorator is a class. It is a class that exposes the same interface(s) as the class it is decorating, that it wants to extend. This is not enough of course, to complete the decoration, it will be necessary to pass an instance of the decorated class to the class which decorates it, often via its constructor. The most common image to visualize this operation is that of Russian dolls nesting in each other. This is also known as onion architecture.

An example with PHP

<?php

class LivingThing{}

class Animal extends LivingThing {}

class Cat extends Animal 
{
    public function __construct(
        public string $name,
        public bool $needToBeBrushed = true,
        public bool $needClawsCutting = true,
        public bool $needToBeFed = true
    ){}
}

interface AnimalShelter
{
    public function adopt(string $name): Animal;
}

class CatShelter implements AnimalShelter
{
    public function adopt(string $name): Cat
    {
        return new Cat($name);
    }
}

class BetterCatShelter implements AnimalShelter
{
    // The decorated instance is given
    public function __construct(private AnimalShelter $decorated)
    {}
     
    public function adopt(string $name): Cat
    {
        // We use the initial behavior of the decorated instance
        $cat = $this->decorated->adopt($name);
        
        // As well as an additional behavior
        $this->groom($cat);
        
        return $cat;
    }
     
    private function groom(Cat $cat)
    {
        $cat->needToBeBrushed = false;
        $cat->needClawsCutting = false;
        $cat->needToBeFed = false;
    }
}

class Me
{
    private Cat $cat;
     
    // Since the decorator has the same interface, 
    // I don't have to change anything in this class to use either
    public function getACatFromShelter(AnimalShelter $shelter, string $catName)
    {
        $this->cat = $shelter->adopt($catName);
    }
     
    public function checkCatStatus(): string
    {
        if (!$this->cat->needToBeBrushed && !$this->cat->needClawsCutting && !$this->cat->needToBeFed) {
            return "All is well for {$this->cat->name}.";
        }
        
        return "Wow... {$this->cat->name} needs attention!";
    }
}

$me = new Me();
$catShelter = new CatShelter();

$me->getACatFromShelter($catShelter, 'Cripper');
echo $me->checkCatStatus(). PHP_EOL;

$betterCatShelter = new BetterCatShelter($catShelter);
$me->getACatFromShelter($betterCatShelter, 'Jinjer');

echo $me->checkCatStatus(). PHP_EOL;

Don't hesitate to play with this example, to mess with it to understand the mechanics. For example, have fun adding a CollarOptionCatShelter that puts a collar on the cat when you pay more.

And try to decorate CatShelter, and/or BetterCatShelter.

Applying the pattern with Symfony services

In Symfony, all classes registered as services are decoratable. In version 6.1 of the framework, here is how to do it (taken from the documentation: https://symfony.com/blog/new-in-symfony-6-1-service-decoration-attributes). You can also refer to our cooperator Robin's talk: https://slides.com/chalasr/symfonybc-promise-demystifed/.

Consider the common case where you want to decorate a service (e.g. Mailer) with a new service that adds logging capabilities to it (e.g. LoggingMailer). Here's how you can set up the decoration with PHP attributes:
Both classes should implement a common interface. Here MailerInterface.

<?php

// src/Mailer/LoggingMailer.php
namespace App\Mailer;

// ...
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;

#[AsDecorator(decorates: Mailer::class)]
class LoggingMailer implements MailerInterface
{
<meta charset="utf-8">    // ...
}

The #[AsDecorator] attribute supports any additional options you may need:

<?php

// ...

#[AsDecorator(
<meta charset="utf-8">    decorates: Mailer::class,
<meta charset="utf-8">    priority: 10,
<meta charset="utf-8">    onInvalid: ContainerInterface::IGNORE_ON_INVALID_REFERENCE,
)]
class LoggingMailer implements MailerInterface
{
<meta charset="utf-8">    // ...
}

If you need to access the decorated service from within the decorator service, add the #[MapDecorated] attribute to one of the service constructor arguments:

<?php
 
// ...
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\MapDecorated;

#[AsDecorator(decorates: Mailer::class)]
class LoggingMailer implements MailerInterface
{
    public function __construct(#[MapDecorated] <meta charset="utf-8">MailerInterface $originalMailer)
    {
    	// ...
    }

    // ...
}
From Baptiste, Antoine, Vincent, Robin, Myself... (and all those who have suffered from it)

I hope this concept is a bit clearer for you now.
For your personal monitoring, don't hesitate to check this Symfony post on the different ways to decorate a service.

Enjoy your code!

The blog

Go further