patron-decorator

Decorator Pattern: adding behavior by composition

  • 5 min

The Decorator is an object that wraps another with the same interface to add behavior. We can stack several decorators and combine responsibilities without creating a subclass for each combination.

Imagine we have a Notifier class that sends emails. We want to add the option to also send by SMS. The easy way is to create a NotifierWithSMS subclass. What if we also want to notify via Facebook? We create NotifierWithFacebook.

But what if we want SMS and Facebook? Do we create NotifierWithSMSAndFacebook? And what if we want SMS and Slack?

This is known as Combinatorial Class Explosion. If we try to cover all possible combinations using inheritance, we end up with hundreds of unmanageable subclasses. Additionally, inheritance is static: we cannot remove the SMS functionality from an object at runtime.

The Decorator pattern allows us to add responsibilities to individual objects dynamically and transparently by wrapping them in “decorator” objects.

The Metaphor: Layers of Clothing

Think of a decorator like clothing.

  • The base object is you.
  • If you’re cold, you put on a sweater.
  • If it’s still cold, you add a coat.
  • If it rains, you add a raincoat.

The chain still offers the same interface but incorporates new responsibilities. To remove a layer we normally rebuild or reconfigure the chain; the pattern does not require decorators to be mutable.

The Problem: The Coffee Shop

The classic example (and the easiest to understand) is the billing system of a Starbuzz coffee shop (wink, wink).

We have a base beverage (PlainCoffee) that costs €1. Customers can order extras: Milk, Chocolate, Cinnamon, Whipped Cream…

If we use inheritance, we would have classes like CoffeeWithMilk, CoffeeWithMilkAndChocolate, CoffeeWithCreamAndCinnamon… That’s crazy.

The Solution: Wrapping Objects

Instead of inheriting, we use Composition.

We define a common interface for the beverage and condiments (IBeverage).

We create a base class for decorators that also implements IBeverage and contains a reference to another IBeverage.

Concrete decorators (Milk, Chocolate) “wrap” the original beverage, add their price, and delegate the rest.

:::

Implementation in C#

Let’s build our flexible coffee shop.

The Component (The Beverage)

public interface IBeverage
{
    string GetDescription();
    double GetCost();
}

// Base implementation (The core of the onion)
public class PlainCoffee : IBeverage
{
    public string GetDescription() => "Plain Coffee";
    public double GetCost() => 1.00;
}

public class DecafCoffee : IBeverage
{
    public string GetDescription() => "Decaf Coffee";
    public double GetCost() => 1.20;
}
Copied!

The Base Decorator

This class is the key. It is a hybrid: it is an IBeverage (so it can be wrapped in turn) and it has an IBeverage (the one it wraps).

public abstract class BeverageDecorator : IBeverage
{
    // The variable that stores the object we are "wrapping"
    protected IBeverage _beverage;

    public BeverageDecorator(IBeverage beverage)
    {
        _beverage = beverage;
    }

    // By default, we delegate to the wrapped beverage.
    // Subclasses will override this to add their behavior.
    public virtual string GetDescription() => _beverage.GetDescription();
    public virtual double GetCost() => _beverage.GetCost();
}
Copied!

The Concrete Decorators (Condiments)

Here we apply the additive logic. Notice that we call base.GetCost() and add our own.

public class Milk : BeverageDecorator
{
    public Milk(IBeverage beverage) : base(beverage) { }

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

    public override double GetCost()
    {
        return _beverage.GetCost() + 0.50; // Milk costs 50 cents
    }
}

public class Chocolate : BeverageDecorator
{
    public Chocolate(IBeverage beverage) : base(beverage) { }

    public override string GetDescription()
    {
        return _beverage.GetDescription() + ", with Chocolate";
    }

    public override double GetCost()
    {
        return _beverage.GetCost() + 0.75;
    }
}
Copied!

Client: Building the Coffee

This is where you see the power. We can combine ingredients however we want at runtime.

class Program
{
    static void Main(string[] args)
    {
        // 1. We order a plain coffee
        IBeverage myCoffee = new PlainCoffee();
        Console.WriteLine($"{myCoffee.GetDescription()} -> {myCoffee.GetCost()}€");

        // 2. The customer wants milk
        // We wrap the coffee in a Milk decorator
        myCoffee = new Milk(myCoffee);
        Console.WriteLine($"{myCoffee.GetDescription()} -> {myCoffee.GetCost()}€");

        // 3. The customer has a sweet tooth and wants chocolate TOO
        // We wrap the (coffee + milk) in Chocolate
        myCoffee = new Chocolate(myCoffee);
        Console.WriteLine($"{myCoffee.GetDescription()} -> {myCoffee.GetCost()}€");
        
        // Final result: Plain Coffee, with Milk, with Chocolate -> 2.25€
    }
}
Copied!

Notice that the myCoffee variable is always of type IBeverage. The client doesn’t care if it’s a simple coffee or a 5-layer monster; it just calls GetCost().

Decorator in Real Life (.NET)

If you have worked with Streams in C#, you have already used this pattern.

// FileStream is the base component (reads bytes from disk)
Stream stream = new FileStream("file.txt", FileMode.Create);

// GZipStream is a DECORATOR (adds compression)
stream = new GZipStream(stream, CompressionLevel.Optimal);

// BufferedStream is ANOTHER DECORATOR (adds in-memory buffer)
stream = new BufferedStream(stream);

// We write to the final 'stream'.
// Buffered -> GZip -> File -> Disk
stream.Write(...); 
Copied!

It is exactly the same principle: chaining functionalities.

Advantages and Disadvantages

AdvantagesDisadvantages
Extensibility: Add functionality without touching existing code (OCP).Complexity: Many small classes. It can be difficult to understand the flow if there are many layers.
Flexibility: Allows combining behaviors at runtime, something impossible with inheritance.Identity: A decorator is not the original object. If your code depends on type checking (if (obj is PlainCoffee)), the Decorator will break it.
SRP: Each class has a single responsibility (the Milk decorator only knows about milk).