Design Patterns: Decorator
The Decorator Pattern allows you to dynamically extend an object's behaviour without modifying its existing code, making it ideal for scenarios like pricing adjustments, UI theming etc etc.
Overview
One of the most powerful design patterns in software engineering is the Decorator Pattern. It allows us to dynamically add functionality to objects without modifying their core structure.
And what better way to illustrate this than with pizza? Pizza is the BEST.
Grab A Pizza This!
Imagine you're building a pizza ordering system. You start with a base pizza, then add toppings dynamically based on customer preferences. Instead of creating a separate class for every possible pizza variation (which would be an actual nightmare), you use decorators to wrap additional functionality around a base object.
This approach aligns with the Single Responsibility Principle (SRP) and keeps your code extensible and maintainable. Need to add a pineapple? Wrap it with a new decorator. Want a thin crust instead? Swap the base component.
To demonstrate this pattern, let's define a Pizza
interface that ensures every pizza component provides a price and a list of toppings:
interface Pizza
{
public function getPrice(): float;
public function getToppings(): array;
}
This bit is optional but I like create an abstract BaseComponent
class that serves as a foundation for crusts. It ensures that all crust types share the same structure.
abstract class BaseComponent implements Pizza
{
protected float $price;
protected array $toppings;
public function getPrice(): float
{
return round($this->price, 2);
}
public function getToppings(): array
{
return $this->toppings;
}
}
And then lets create some concrete classes (don’t worry, it’s nothing too complicated, it’s the pizza bases).
class NewYorkStyleCrust extends BaseComponent
{
protected float $price = 4.49;
protected array $toppings = [];
}
class ThickCrust extends BaseComponent
{
protected float $price = 5.99;
protected array $toppings = [];
}
class ThinCrust extends BaseComponent
{
protected float $price = 3.49;
protected array $toppings = [];
}
After that, let’s deal with potential toppings, at this point we’ve already got an interface we can use, and again, completely optional, I prefer to use an abstract class to eliminate any duplicate code (there was other ways you can tackle this, or can decide to not tackle it entirely, I guess).
abstract class ToppingDecorator implements Pizza
{
protected float $price;
protected array $toppings;
public function __construct(protected Pizza $pizza)
{
}
public function getPrice(): float
{
return round($this->pizza->getPrice() + $this->price, 2);
}
public function getToppings(): array
{
return array_merge(
$this->pizza->getToppings(),
$this->toppings
);
}
}
With the abstract in place, what’s next?!? You guessed it, it’s time for some concrete classes, this time for pizza toppings!
class Ham extends ToppingDecorator
{
protected float $price = 1.29;
protected array $toppings = [
'Ham'
];
}
class Mushroom extends ToppingDecorator
{
protected float $price = 0.99;
protected array $toppings = [
'Mushrooms'
];
}
class Pepperoni extends ToppingDecorator
{
protected float $price = 1.49;
protected array $toppings = [
'Pepperoni'
];
}
class Pineapple extends ToppingDecorator
{
protected float $price = 2.99;
protected array $toppings = [
'Pineapple'
];
}
class Sweetcorn extends ToppingDecorator
{
protected float $price = 0.49;
protected array $toppings = [
'Sweetcorn'
];
}
Okay, okay I know what you’re thinking—who the fuck wants pineapple on their pizza? Honestly, I’ve got no idea but I can show you how the code above can be put together, give you some pros and cons, show an alternative, some few real-world business problems this pattern solves and round it off with a conclusion?
Sound good? No? Tough, we’re doing it anyway.
Putting It All Together
We can now build a pizza dynamically by wrapping it in multiple topping decorators, like so:
it('Can make a new york style pizza with ham and mushrooms', function (): void {
$pizza = new Ham(
new Mushroom(
new NewYorkStyleCrust()
)
);
expect($pizza->getPrice())->toBe(6.77);
expect($pizza->getToppings())->toBe(['Mushrooms', 'Ham']);
});
it('Can make a thick crust pizza with ham and 5 pineapples', function (): void {
$pizza = new Ham(
new Pineapple(
new Pineapple(
new Pineapple(
new Pineapple(
new Pineapple(
new ThickCrust()
)
)
)
)
)
)
);
expect($pizza->getPrice())->toBe(22.23);
expect($pizza->getToppings())->toBe([
'Pineapple',
'Pineapple',
'Pineapple',
'Pineapple',
'Pineapple',
'Ham'
]);
});
The structure of the nested decorators in the above test closely resembles a one-way linked list. Each topping wraps the previous component, much like how each node in a linked list contains a reference to the next node. In this case:
The base crust (
ThickCrust
) acts as the head node.Each topping (
Pineapple
,Ham
) is a node that points to the next one.The final wrapper (
Ham
) serves as the last node, allowing us to traverse back through the structure to retrieve all toppings and the final price.
This recursive wrapping ensures that each added decorator enhances the existing object without altering its fundamental structure, just as a linked list allows dynamic node additions without modifying the underlying structure.
Pros
✅ When you need to dynamically add or remove behaviours at runtime.
✅ When subclassing would lead to an explosion of subclasses.
✅ When you want to follow the Open-Closed Principle (open for extension, closed for modification).
✅ When you need to change something (i.e the cost of a pizza topping), you can do this in one place rather than modifying multiple areas of code.
Cons
❌ When you have a fixed set of configurations (a simple array might be enough).
❌ When performance is critical, as decorators add extra method calls.
❌ When too many nested decorators make debugging a nightmare.
A Simpler Alternative
It’s not perfect but it gets the job done right?
class PizzaMaker
{
private array $crustPrices = [
'Thin' => 3.49,
'NewYorkStyle' => 4.49,
'Thick' => 5.99,
];
private array $toppingPrices = [
'Ham' => 1.29,
'Mushrooms' => 0.99,
'Pepperoni' => 1.49,
'Pineapple' => 2.99,
'Sweetcorn' => 0.49,
];
public function makePizza(string $crust, array $toppings): array
{
$price = $this->crustPrices[$crust] ?? 0.00;
$toppingsList = [];
foreach ($toppings as $topping) {
$price += $this->toppingPrices[$topping] ?? 0.00;
$toppingsList[] = $topping;
}
return [
'price' => round($price, 2),
'toppings' => $toppingsList,
];
}
}
What happens if new crusts or toppings are added? The class needs manual modification, violating the Open-Closed Principle (OCP). This could lead to maintenance headaches.
The class is procedural in nature rather than truly object-oriented but it gets the job done, like so:
$pizzaMaker = new PizzaMaker();
$result = $pizzaMaker->makePizza('Thin', ['Ham', 'Pineapple']);
$price = $result['price']; // 7.77
$toppings = $result['toppings']; // ['Ham', 'Pineapple']
That’s it, it does work, however what’s the impact after 20 changes? Maybe there’s changes to business logic? What about 5 years down the line?
Real World Solutions
Ok, how often are you going to be creating a Pizza decorator? Never? Yep, probably, here’s a few real world solutions, the decorator pattern could solve and why:
Formatting & Output Processing (e.g., HTML, JSON, Encryption)
Problem:
You need to modify how data is output without changing the core object.
Solution:
Wrap the original data object in decorators that transform it step by step.
Extending Logging Functionality (e.g., Monolog, PSR-3)
Problem:
You need to dynamically add extra behaviours to logging (e.g., sending logs to Slack, adding timestamps, masking sensitive data).
Solution:
Decorators allow you to wrap existing loggers and modify their behaviour dynamically.
Dynamic Pricing & Discounts in E-commerce
Problem: You need to apply multiple pricing adjustments (e.g., discounts, taxes, promotions) dynamically without modifying the base product class.
Solution: Use decorators to wrap the base price and apply modifications in layers.
Stu’s Final Thoughts
Deep down we’re all alike, I believe that people have similar problems, thoughts, and feelings, and that everyone wants to be happy.
Err… Also, the Decorator Pattern is a great way to keep your code modular, reusable, and easy to extend. It requires more classes, but it gives you the ability to modify behaviour at runtime without touching existing code.
That said, don’t be afraid to reach for a simple solution. Remember the goal is to solve a business problem, if you’re just flexing your OOPectorals, what’s the point? Keep it simple, until you have a legit reason not to.
It depends….
🍕 Code smart. Eat well. Be Excellent To Each Other. Party On Dude. 🍕
Want to see the Decorator Pattern in action? Check out this simple PHP example: Github