Skip to content Skip to footer

Decorator Design Pattern: Adding Behavior Dynamically

You’ve built a text processing system. Now you need to add features: bold formatting, italic, encryption, compression. Do you create subclasses for every combination? BoldText, ItalicText, BoldItalicText, EncryptedBoldText… The Decorator pattern lets you add behaviors dynamically by wrapping objects, avoiding the subclass explosion problem.

Problem

Consider a coffee ordering system:

public abstract class Coffee
{
    public abstract string GetDescription();
    public abstract decimal GetCost();
}

public class SimpleCoffee : Coffee
{
    public override string GetDescription() => "Simple Coffee";
    public override decimal GetCost() => 2.00m;
}

public class CoffeeWithMilk : Coffee
{
    public override string GetDescription() => "Coffee with Milk";
    public override decimal GetCost() => 2.50m;
}

public class CoffeeWithSugar : Coffee
{
    public override string GetDescription() => "Coffee with Sugar";
    public override decimal GetCost() => 2.25m;
}

What about coffee with milk AND sugar? You’d need CoffeeWithMilkAndSugar. Add whipped cream, and you need even more classes. This approach doesn’t scale.

Solution

The Decorator pattern attaches additional responsibilities to an object dynamically. It provides a flexible alternative to subclassing for extending functionality.

Definition: The Decorator pattern lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors.

Instead of creating subclasses for every combination, you create decorator classes that wrap the original object and add functionality. Decorators can be stacked to combine multiple behaviors.

Pseudocode

Component (ICoffee)
    ↓ implemented by
ConcreteComponent (SimpleCoffee) and Decorator (CoffeeDecorator)
    ↓ Decorator wraps
Component
    ↓ Decorator adds
Additional behavior

Examples

Here’s a complete, runnable C# implementation:

using System;

// Component interface
public interface ICoffee
{
    string GetDescription();
    decimal GetCost();
}

// Concrete component
public class SimpleCoffee : ICoffee
{
    public string GetDescription() => "Simple Coffee";
    public decimal GetCost() => 2.00m;
}

// Base decorator
public abstract class CoffeeDecorator : ICoffee
{
    protected ICoffee _coffee;

    protected CoffeeDecorator(ICoffee coffee)
    {
        _coffee = coffee;
    }

    public virtual string GetDescription()
    {
        return _coffee.GetDescription();
    }

    public virtual decimal GetCost()
    {
        return _coffee.GetCost();
    }
}

// Concrete decorators
public class MilkDecorator : CoffeeDecorator
{
    public MilkDecorator(ICoffee coffee) : base(coffee) { }

    public override string GetDescription()
    {
        return _coffee.GetDescription() + ", Milk";
    }

    public override decimal GetCost()
    {
        return _coffee.GetCost() + 0.50m;
    }
}

public class SugarDecorator : CoffeeDecorator
{
    public SugarDecorator(ICoffee coffee) : base(coffee) { }

    public override string GetDescription()
    {
        return _coffee.GetDescription() + ", Sugar";
    }

    public override decimal GetCost()
    {
        return _coffee.GetCost() + 0.25m;
    }
}

public class WhippedCreamDecorator : CoffeeDecorator
{
    public WhippedCreamDecorator(ICoffee coffee) : base(coffee) { }

    public override string GetDescription()
    {
        return _coffee.GetDescription() + ", Whipped Cream";
    }

    public override decimal GetCost()
    {
        return _coffee.GetCost() + 0.75m;
    }
}

// Usage
class Program
{
    static void Main()
    {
        // Simple coffee
        ICoffee coffee = new SimpleCoffee();
        Console.WriteLine($"{coffee.GetDescription()}: ${coffee.GetCost()}");

        // Coffee with milk
        coffee = new MilkDecorator(coffee);
        Console.WriteLine($"{coffee.GetDescription()}: ${coffee.GetCost()}");

        // Coffee with milk and sugar
        coffee = new SugarDecorator(coffee);
        Console.WriteLine($"{coffee.GetDescription()}: ${coffee.GetCost()}");

        // Coffee with everything
        coffee = new WhippedCreamDecorator(coffee);
        Console.WriteLine($"{coffee.GetDescription()}: ${coffee.GetCost()}");

        // Or create in one go
        var fancyCoffee = new WhippedCreamDecorator(
            new SugarDecorator(
                new MilkDecorator(
                    new SimpleCoffee()
                )
            )
        );
        Console.WriteLine($"\nFancy: {fancyCoffee.GetDescription()}: ${fancyCoffee.GetCost()}");
    }
}

Output:

Simple Coffee: $2.00
Simple Coffee, Milk: $2.50
Simple Coffee, Milk, Sugar: $2.75
Simple Coffee, Milk, Sugar, Whipped Cream: $3.50

Fancy: Simple Coffee, Milk, Sugar, Whipped Cream: $3.50

Real-World Example: Text Processing

// Component
public interface IText
{
    string GetText();
}

// Concrete component
public class PlainText : IText
{
    private string _text;

    public PlainText(string text)
    {
        _text = text;
    }

    public string GetText() => _text;
}

// Base decorator
public abstract class TextDecorator : IText
{
    protected IText _text;

    protected TextDecorator(IText text)
    {
        _text = text;
    }

    public virtual string GetText()
    {
        return _text.GetText();
    }
}

// Concrete decorators
public class BoldDecorator : TextDecorator
{
    public BoldDecorator(IText text) : base(text) { }

    public override string GetText()
    {
        return $"<b>{_text.GetText()}</b>";
    }
}

public class ItalicDecorator : TextDecorator
{
    public ItalicDecorator(IText text) : base(text) { }

    public override string GetText()
    {
        return $"<i>{_text.GetText()}</i>";
    }
}

public class UnderlineDecorator : TextDecorator
{
    public UnderlineDecorator(IText text) : base(text) { }

    public override string GetText()
    {
        return $"<u>{_text.GetText()}</u>";
    }
}

// Usage
var text = new PlainText("Hello World");
var formatted = new UnderlineDecorator(
    new ItalicDecorator(
        new BoldDecorator(text)
    )
);
Console.WriteLine(formatted.GetText()); // <u><i><b>Hello World</b></i></u>

Applications

Enterprise Use Cases:

  1. Stream Processing: Adding functionality to I/O streams (buffering, compression, encryption)
  2. UI Components: Adding behaviors to visual components (scrolling, borders, tooltips)
  3. HTTP Middleware: Adding cross-cutting concerns (logging, authentication, caching)
  4. Data Validation: Stacking validation rules on data objects
  5. Caching Layers: Adding caching behavior to data access objects
  6. Security: Adding encryption, authentication, or authorization to services
  7. Logging and Monitoring: Adding logging behavior to existing services
  8. Formatting: Adding formatting to text or data objects

Benefits:

  • More flexible than inheritance—add and remove responsibilities at runtime
  • Avoids feature-laden classes high in the hierarchy
  • Allows mixing and matching features by combining decorators
  • Follows the Single Responsibility Principle—each decorator has one job
  • Follows the Open/Closed Principle—open for extension, closed for modification

When to Use:

  • You need to add responsibilities to objects dynamically and transparently
  • You need to add responsibilities that can be withdrawn
  • Extension by subclassing is impractical (too many combinations)
  • You want to add behavior to individual objects without affecting others

Key Insight: Decorators wrap objects and add behavior. They can be stacked to combine multiple behaviors. The client code treats decorated objects the same as undecorated ones, thanks to the shared interface.

Leave a Comment