Skip to Content
Course content

373: The Decorator Pattern (Structural)

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Now, before we dive in, I want to clear something up. When Python developers hear "decorator," they usually think of the @decorator syntax used to wrap functions. While that's a similar concept, the Decorator Design Pattern is a structural pattern. It's about wrapping objects at runtime to add new behaviors without changing the original class or creating a massive, confusing inheritance tree.

The inheritance trap

I remember working on a notification system a few years back where I fell right into the trap I'm about to show you. I wanted a system that could send alerts via Email, SMS, and Slack. I started with a simple interface and then tried to use inheritance to handle the combinations.

class Notifier:
    def send(self, message):
        pass

class EmailNotifier(Notifier):
    def send(self, message):
        print(f"Sending Email: {message}")

class SMSNotifier(Notifier):
    def send(self, message):
        print(f"Sending SMS: {message}")

# Now I want both... so I make a new class?
class EmailAndSMSNotifier(Notifier):
    def send(self, message):
        print(f"Sending Email: {message}")
        print(f"Sending SMS: {message}")

This felt fine for two options. But then the product manager asked for Slack, PagerDuty, and WhatsApp. If I kept going this way, I'd end up with EmailAndSMSAndSlackNotifier, SMSAndSlackAndWhatsAppNotifier, and so on. It's a combinatorial explosion. I was writing the same print statements over and over, and the class hierarchy was becoming a nightmare.

Wrapping objects for flexibility

The fix is to stop thinking about "is-a" (inheritance) and start thinking about "has-a" (composition). Instead of creating a class for every combination, I'll create a base decorator that implements the same interface as the Notifier but holds a reference to another Notifier object inside it.

class NotifierDecorator(Notifier):
    def __init__(self, wrapped_notifier):
        self._wrapped = wrapped_notifier

    def send(self, message):
        # By default, just delegate to the wrapped object
        self._wrapped.send(message)

Now, I can create specific decorators that add their own logic before or after calling the wrapped object. This is where the magic happens: the decorator doesn't care if it's wrapping a base Notifier or another decorator.

class SMSDecorator(NotifierDecorator):
    def send(self, message):
        super().send(message) # Send the previous ones first
        print(f"Sending SMS: {message}")

class SlackDecorator(NotifierDecorator):
    def send(self, message):
        super().send(message)
        print(f"Sending Slack: {message}")

Stacking behaviors at runtime

Here is the part I love about this pattern. I don't have to define the combinations in my code; I can define them when the application is actually running based on user preferences.

# Basic setup
notifier = EmailNotifier()

# User wants Email AND SMS
notifier = SMSDecorator(notifier)

# User now decides they also want Slack
notifier = SlackDecorator(notifier)

# This will trigger Email -> SMS -> Slack
notifier.send("System Overload!")

Notice how notifier is still just a Notifier type. The rest of my application doesn't need to know that it's actually a SlackDecorator wrapping an SMSDecorator wrapping an EmailNotifier. It just calls .send() and the chain reacts.

A quick note on order

One thing I learned the hard way: the order of wrapping matters. If I have a LoggingDecorator that logs the start and end of a notification, wrapping it around the others will capture the entire chain. Wrapping it inside the others will only log that specific step. Always think about whether your decorator should execute its logic before or after the super().send() call.




📋 Practical Task

Building a Layered File Data Processor

You are building a system that processes strings of data before they are written to a file. Instead of creating dozens of classes for every possible combination of processing, use the Decorator Pattern to implement the following:

  • A base DataProvider class with a get_data() method that returns a simple string (e.g., "Base data content").
  • A DataDecorator base class that wraps a DataProvider.
  • An EncryptionDecorator that takes the output of get_data() and appends " [ENCRYPTED]" to it.
  • A CompressionDecorator that takes the output of get_data() and appends " [COMPRESSED]" to it.

Your Goal: Create a sequence where you wrap the base provider in both encryption and compression, then call get_data() to prove that the string has been modified by both layers.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.