If you’ve read some of my earlier blog posts, you know I’m a sucker for a good software engineering pattern. There’s just something about using a well-designed pattern to solve real problems that makes me tick. Implementing a design pattern and seeing it work in action has that je ne sais quoi that will never get old to me. It’s like an “aha” moment, a dopamine hit.
I implemented the decorator design pattern recently, and gosh dang golly jeepers, did it feel awesome to code. So awesome, in fact, that I wanted to write about it here. Buckle up.
Structural Design Patterns
The decorator pattern is a structural design pattern. Structural design patterns aim to make object relationships more flexible, unified, and extensible, especially as software systems grow. You could think about structural design patterns as addressing the question: how do those objects fit together?
You could, as an example, design a system where a user object depends on an email service. As your system grows, you could add dependencies such as a push notification service, profile settings, and notification preferences. If the user object is coupled to a service such as Mailgun or SendGrid for sending emails, and that service goes down, you may need to modify your user object, which can lead to a brittle design. The user object, after all, shouldn’t need to know how it’s sending an email. The user object only needs to know to send an email and to allow the appropriate service to do so.
This is where structural design patterns shine: how does the user object and the email service fit together? That’s the type of question it helps to address.
With that context, what about the decorator pattern? What is it and what problem does it solve?
The Decorator Pattern
When you want to extend an object’s behavior, especially when you want to add features dynamically, the decorator pattern is extremely useful. The decorator pattern uses composition rather than inheritance to extend an object’s features and behavior without modifying its original code.
What do all of those words mean? Let’s take a look at an example.
Say we are writing an application for an ice cream shop. In addition to getting n scoops of ice cream, you can get a lot of different toppings: sprinkles, fudge, nuts, Reese’s (my fave), etc. All toppings are optional and combinable. I would opt for chocolate ice cream topped with Reese’s, while my son would certainly pick strawberry ice cream with sprinkles and fudge.
If we decided to use inheritance, we would need to create subclasses for each permutation of ice cream toppings. It might look something like this:
from abc import ABC, abstractmethod
class IceCream:
def __init__(self, flavor: str):
self.flavor = flavor
def get_price(self) -> float:
return 3.00
def get_description(self) -> str:
return f"{self.flavor} ice cream"
class Topping(ABC):
@abstractmethod
def get_price(self) -> float:
pass
class Fudge(Topping):
def get_price(self):
return 1.00
class Sprinkles(Topping):
def get_price(self):
return 0.50
class FudgeAndSprinkles(Topping):
def get_price(self):
return 1.50
Every time you add a new permutation, you’re creating subclasses for every possible combination of notification preferences. If you only have fudge and sprinkles, no problem. As you add toppings, the number of subclasses you need explodes. With just 10 toppings, you have 210 permutations, which would make 1,024 subclasses! Ain’t nobody got time for that!
You might ask, why not just have an IceCream class that takes in a bunch of optional arguments in its constructor? Glad you asked. Ultimately, this would violate the Open/Closed Principle. If you ever needed to change the price of fudge, your IceCream class isn’t “closed for modification.“ This approach would also lead to large if/else statements that are difficult to read and debug, and don’t follow best practices.
To solve this, the decorator pattern uses composition instead of inheritance. Objects are wrapped by decorators that add new behavior before or after delegating to the original object. The decorator implements the same interface as the object it wraps, allowing the client to use it interchangeably with that object as needed. Let’s see how this works in action:
from abc import ABC, abstractmethod
class IceCreamComponent(ABC):
@abstractmethod
def get_price(self) -> float:
pass
@abstractmethod
def get_description(self) -> str:
pass
class IceCream(IceCreamComponent):
def __init__(self, flavor: str):
self.flavor = flavor
def get_price(self):
return 3.00
def get_description(self):
return f"{self.flavor} ice cream"
class ToppingDecorator(IceCreamComponent):
def __init__(self, topping: IceCreamComponent):
self._topping = topping
def get_price(self):
return self._topping.get_price()
def get_description(self):
return self._topping.get_description()
class Fudge(ToppingDecorator):
def get_price(self):
price = super().get_price()
return price + 1.00
def get_description(self):
return super().get_description() + " + fudge"
class Sprinkles(ToppingDecorator):
def get_price(self):
price = super().get_price()
return price + 0.50
def get_description(self):
return super().get_description() + " + sprinkles"
class Reeces(ToppingDecorator):
def get_price(self):
price = super().get_price()
return price + 1.50
def get_description(self):
return super().get_description() + " + Reece’s"
Ok, great, thanks for the code, Jacob. How do I use this?
Each topping is now its own class, built on ToppingDecorator which is constructed with an IceCreamComponent. This allows us to build our perfect ice cream however we see fit, while also allowing us to add more toppings later or adjust topping prices without affecting the base ice cream class or any other topping.
You can really see the power of the decorator design pattern when you start playing around with it. The code below shows how to use the topping decorators to dynamically add information to the ice cream, building your perfect scoop.
ice_cream = IceCream("strawberry")
print(ice_cream.get_description() + f" (${ice_cream.get_price():.2f})")
# Output: strawberry ice cream ($3.00)
ice_cream_with_fudge = Fudge(ice_cream)
print(ice_cream_with_fudge.get_description() + f" (${ice_cream_with_fudge.get_price():.2f})")
# Output: strawberry ice cream + fudge ($4.00)
ice_cream_with_sprinkles = Sprinkles(ice_cream_with_fudge)
print(ice_cream_with_sprinkles.get_description() + f" (${ice_cream_with_sprinkles.get_price():.2f})")
# Output: strawberry ice cream + fudge + sprinkles ($4.50)
In sum: I scream, you scream, we all scream for ice cream! 😁
📝 In Case You Missed It…
I’ve been nerding out over the SOLID Principles. The “D” is the Dependency Inversion Principle.
Otherwise, I’ve barely added to the mind garden! Will try to tend to it this next week.
📚 What I’m Consuming
AWS’s Werner Vogels’ Keynote speech at the re:Invent 2025 conference (video): He talked about the keys to the “Renaissance developer,” those attributes that make any developer timeless.
Currencies (On Motivating Different People) by Ed Batista nicely breaks down the different resources for motivation. Key phrase for me when deciding what resources to employ: “Any number of the currencies above can be dispensed like ‘dog biscuits or jelly beans,’ but will the result be motivation or merely movement?”
Peace be with you,
Jacob
psst… hey, could you forward this to someone you think would find it valuable? I’d greatly appreciate it!
