Decorator Pattern: Enhancing Behavior Dynamically

Learn how the Decorator Pattern allows behavior extension without modifying code. Includes C# and Python examples, UML, real-world use cases, and pros & cons.

The Decorator Pattern is a structural design pattern, meaning it focuses on how objects are composed to form larger structures. Its superpower lies in allowing new behavior to be added to individual objects at runtime—without modifying their existing code. Instead of inheritance, it uses composition by wrapping objects in "decorator" classes.

🧠 When to Use

This pattern shines when:
  • You want to extend functionality without altering existing classes.
  • You need to add responsibilities dynamically and transparently, without affecting other objects.
  • You want to avoid the limitations of subclassing, such as deep inheritance trees.
  • You want to combine behaviors in a flexible way, allowing for dynamic composition of behaviors.
  • You want to adhere to the Open/Closed Principle, meaning classes should be open for extension but closed for modification.

📌 Key Characteristics

  • Uses composition over inheritance.
  • Allows behavior to be added dynamically at runtime.
  • Can be stacked to combine multiple behaviors.
  • Adheres to the Single Responsibility Principle by allowing each decorator to focus on a specific behavior.

🔧 UML Diagram

[UML diagram placeholder: Component -> ConcreteComponent, Decorator (wraps Component) -> ConcreteDecorator]

💻 Code Example

// Component
public interface INotifier {
    void Send(string message);
}

// Concrete Component
public class EmailNotifier : INotifier {
    public void Send(string message) {
        Console.WriteLine("Sending Email: " + message);
    }
}

// Base Decorator
public class NotifierDecorator : INotifier {
    protected INotifier wrappee;
    public NotifierDecorator(INotifier notifier) {
        this.wrappee = notifier;
    }
    public virtual void Send(string message) {
        wrappee.Send(message);
    }
}

// Concrete Decorator
public class SMSNotifier : NotifierDecorator {
    public SMSNotifier(INotifier notifier) : base(notifier) {}
    public override void Send(string message) {
        base.Send(message);
        Console.WriteLine("Sending SMS: " + message);
    }
}

// Usage
INotifier notifier = new SMSNotifier(new EmailNotifier());
notifier.Send("Hello World!");
# Component
class Notifier:
    def send(self, message):
        pass

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

# Base Decorator
class NotifierDecorator(Notifier):
    def __init__(self, wrappee):
        self.wrappee = wrappee

    def send(self, message):
        self.wrappee.send(message)

# Concrete Decorator
class SMSNotifier(NotifierDecorator):
    def send(self, message):
        super().send(message)
        print(f"Sending SMS: {message}")

# Usage
notifier = SMSNotifier(EmailNotifier())
notifier.send("Hello World!")

🎁 How It Works

You create a base component interface or class (e.g., INotifier) and concrete implementations (e.g., EmailNotifier). To add new behaviors, you use decorator classes that wrap the original object and add their own enhancements before or after delegating to it. For instance, EmailNotifier sends an email.SMSNotifier wraps EmailNotifier and also sends an SMS—extending the behavior. 📦 So now, calling Send() on the SMSNotifier still sends the email via the wrapped EmailNotifier, plus the SMS.

🍕 Another Example: Pizza Toppings with Decorator

// Component
public interface IPizza
{
    string GetDescription();
    double GetCost();
}

// Concrete Component
public class Margherita : IPizza
{
    public string GetDescription() => "Margherita";
    public double GetCost() => 5.00;
}

// Base Decorator
public abstract class ToppingDecorator : IPizza
{
    protected IPPizza _pizza;

    public ToppingDecorator(IPPizza pizza)
    {
        _pizza = pizza;
    }

    public virtual string GetDescription() => _pizza.GetDescription();
    public virtual double GetCost() => _pizza.GetCost();
}

// Concrete Decorators
public class Cheese : ToppingDecorator
{
    public Cheese(IPPizza pizza) : base(pizza) { }

    public override string GetDescription() => base.GetDescription() + ", Cheese";
    public override double GetCost() => base.GetCost() + 1.25;
}

public class Olives : ToppingDecorator
{
    public Olives(IPPizza pizza) : base(pizza) { }

    public override string GetDescription() => base.GetDescription() + ", Olives";
    public override double GetCost() => base.GetCost() + 0.75;
}

// Usage
var pizza = new Cheese(new Olives(new Margherita()));
Console.WriteLine(pizza.GetDescription()); // Output: Margherita, Olives, Cheese
Console.WriteLine($"Total: ${pizza.GetCost()}"); // Output: Total: $7.0
from abc import ABC, abstractmethod

# Component
class Pizza(ABC):
    @abstractmethod
    def get_description(self):
        pass

    @abstractmethod
    def get_cost(self):
        pass

# Concrete Component
class Margherita(Pizza):
    def get_description(self):
        return "Margherita"

    def get_cost(self):
        return 5.00

# Base Decorator
class ToppingDecorator(Pizza):
    def __init__(self, pizza):
        self._pizza = pizza

    def get_description(self):
        return self._pizza.get_description()

    def get_cost(self):
        return self._pizza.get_cost()

# Concrete Decorators
class Cheese(ToppingDecorator):
    def get_description(self):
        return super().get_description() + ", Cheese"

    def get_cost(self):
        return super().get_cost() + 1.25

class Olives(ToppingDecorator):
    def get_description(self):
        return super().get_description() + ", Olives"

    def get_cost(self):
        return super().get_cost() + 0.75

# Usage
pizza = Cheese(Olives(Margherita()))
print(pizza.get_description())  # Output: Margherita, Olives, Cheese
print(f"Total: ${pizza.get_cost():.2f}")  # Output: Total: $7.00
Note: Decorators can be stacked, allowing multiple behaviors to be combined dynamically.
Tip: Use decorators when you need to add responsibilities to objects without modifying their code.

🌍 Real-World Examples

  • UI Frameworks: Adding scrollbars, borders, or shadows to elements.

    Example: In a GUI library, you can wrap a button with a border decorator to add a border without changing the button's code.

  • Java IO: BufferedReader wraps FileReader for extra behavior.

    Example: In Java, BufferedReader wraps FileReader to add buffering capabilities, improving performance.

  • Security: Adding logging, validation, or access control dynamically.

    Example: In a web application, you can wrap a service with a security decorator to log access attempts or validate user permissions before executing the service's methods.

⚖️ Pros and Cons of the Decorator Pattern

✅ Pros ❌ Cons
Supports Open/Closed Principle – extend behavior without modifying original code. Can result in a large number of small classes that are hard to manage.
Allows dynamic composition of behaviors at runtime. Nested decorators can make debugging more complex due to call stack layering.
Reduces the need for deep inheritance hierarchies. Understanding object behavior may require tracing through multiple layers.
Provides greater flexibility than subclassing for feature extension. Decorators must match the component interface exactly, which may be restrictive.
Allows combining multiple concerns (e.g., logging, security, caching) in a modular way. If not managed well, can introduce performance overhead with many wrappers.

📊 Decorator vs Inheritance

“Decorator allows combining behavior in a flexible way without the rigidity of inheritance trees”—is a concise way of capturing the key distinction between the Decorator Pattern and classical inheritance.

  • Inheritance builds new behavior by creating subclasses. But this can lead to deep, rigid “inheritance trees” where every variation requires a new subclass.
  • Decorator Pattern, on the other hand, uses composition: it wraps objects in layers (decorators), each adding some behavior. This avoids bloated inheritance chains.

🧱 Why Decorators Are More Flexible

  • You can add or change behavior at runtime by simply wrapping an object in one or more decorators.
  • Behaviors are modular and reusable. You can stack decorators in different combinations to create exactly the functionality you need—like adding ingredients to a pizza 🍕.
  • This aligns with the Open/Closed Principle: systems are open for extension but closed for modification.

🆚 Inheritance: The Rigidity Problem

  • In inheritance, if you want an object that sends both emails and SMS, you’d have to subclass in a specific way—or worse, create multiple hybrids.
  • In decorators, you’d just wrap an EmailNotifier with an SMSNotifier. Simple. Reversible. Swappable.

🧪 Try This!

Can you add another decorator to send messages via Slack? Try extending the chain!

❓ Quiz

Test your understanding of the Decorator Pattern:

📚 Conclusion

The Decorator Pattern is a powerful tool for adding functionality to objects dynamically. It promotes flexibility and adheres to key design principles like the Open/Closed Principle. By using decorators, you can create complex behaviors without the pitfalls of deep inheritance hierarchies.

🔗 What's Next?

Stay tuned for our next design pattern: Proxy Pattern – Control Access and Enhance Behavior