Skip to main content

Decorator Pattern

Definition​

The Decorator attaches additional responsibilities to an object dynamically. It provides a flexible alternative to subclassing for extending functionality. Decorators wrap the original object and add behavior before/after delegating to it.

Coffee Shop Example​

A customer orders a base coffee (Espresso, Latte) and adds toppings (Extra Shot, Whipped Cream, Oat Milk, Caramel Syrup). Each topping adds cost and modifies the description. Instead of creating EspressoWithWhippedCreamAndOatMilk, we wrap objects dynamically.

Structure​

Component Interface​

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

Concrete Components β€” Base Coffees​

public class Espresso : ICoffee
{
public string GetDescription() => "Espresso";
public decimal GetCost() => 2.50m;
}

public class Latte : ICoffee
{
public string GetDescription() => "Latte";
public decimal GetCost() => 4.00m;
}

public class ColdBrew : ICoffee
{
public string GetDescription() => "Cold Brew";
public decimal GetCost() => 3.75m;
}

Base Decorator​

public abstract class CoffeeDecorator : ICoffee
{
protected readonly ICoffee _coffee;

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

public virtual string GetDescription() => _coffee.GetDescription();
public virtual decimal GetCost() => _coffee.GetCost();
}

Concrete Decorators β€” Toppings​

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

public override string GetDescription() =>
$"{_coffee.GetDescription()} + Extra Shot";

public override decimal GetCost() =>
_coffee.GetCost() + 0.75m;
}

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

public override string GetDescription() =>
$"{_coffee.GetDescription()} + Whipped Cream";

public override decimal GetCost() =>
_coffee.GetCost() + 0.50m;
}

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

public override string GetDescription() =>
$"{_coffee.GetDescription()} + Oat Milk";

public override decimal GetCost() =>
_coffee.GetCost() + 0.60m;
}

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

public override string GetDescription() =>
$"{_coffee.GetDescription()} + Caramel Syrup";

public override decimal GetCost() =>
_coffee.GetCost() + 0.40m;
}

Client β€” Building the Order​

// Simple order: just a Latte
ICoffee order1 = new Latte();
Console.WriteLine($"{order1.GetDescription()} β€” ${order1.GetCost():F2}");
// Latte β€” $4.00

// Fancy order: Latte + Oat Milk + Extra Shot + Whipped Cream
ICoffee order2 = new WhippedCream(
new ExtraShot(
new OatMilk(
new Latte())));
Console.WriteLine($"{order2.GetDescription()} β€” ${order2.GetCost():F2}");
// Latte + Oat Milk + Extra Shot + Whipped Cream β€” $5.85

// Cold Brew with Caramel
ICoffee order3 = new CaramelSyrup(new ColdBrew());
Console.WriteLine($"{order3.GetDescription()} β€” ${order3.GetCost():F2}");
// Cold Brew + Caramel Syrup β€” $4.15
danger

The nested constructor syntax (new W(new X(new Y(new Z())))) gets hard to read with many layers. Consider a Builder or extension methods for fluent construction:

// Fluent approach with extension methods
ICoffee order = new Latte()
.WithOatMilk()
.WithExtraShot()
.WithWhippedCream();

public static class CoffeeExtensions
{
public static ICoffee WithExtraShot(this ICoffee c) => new ExtraShot(c);
public static ICoffee WithWhippedCream(this ICoffee c) => new WhippedCream(c);
public static ICoffee WithOatMilk(this ICoffee c) => new OatMilk(c);
public static ICoffee WithCaramelSyrup(this ICoffee c) => new CaramelSyrup(c);
}

Decorator vs Inheritance Explosion​

ApproachClasses needed for 3 coffees + 4 toppings
Inheritance (every combo)3 Γ— 2⁴ = 48 classes
Decorator3 coffees + 4 decorators = 7 classes

.NET Real-World Usage​

  • Stream decorators β€” BufferedStream, GZipStream, CryptoStream wrap other streams
  • HttpClient handlers β€” DelegatingHandler chain in ASP.NET Core
  • ASP.NET Core Middleware β€” each middleware wraps the next
  • IAsyncEnumerable with ConfigureAwait and cancellation β€” decorator-like chaining

When to Use​

  • You need to add behavior to objects dynamically at runtime
  • You want to avoid a permanent subclass for every feature combination
  • Adding and removing responsibilities should be possible without changing existing code

When NOT to Use​

  • There's a fixed, small number of combinations β€” plain classes are simpler
  • The decorator chain gets too deep β€” debugging becomes difficult
  • The added behavior isn't composable (decorators that conflict with each other)

Key Takeaways​

  • Decorator wraps an object and delegates to it, adding behavior before/after
  • Both the component and decorator implement the same interface
  • Decorators can be stacked in any order and any number
  • It's the OCP in action β€” extend behavior without modifying existing classes

Interview Questions​

Q: How is Decorator different from Adapter? Decorator extends behavior without changing the interface. Adapter changes the interface to make incompatible things work together. Both wrap objects, but their intent is different.

Q: How is Decorator different from Proxy? Structurally they look the same β€” both wrap an object. But Decorator adds behavior, while Proxy controls access (lazy loading, access control, caching). Proxy often manages the lifecycle of the wrapped object; Decorator doesn't.

Q: What's the connection between Decorator and middleware in ASP.NET Core? ASP.NET Core middleware is the Decorator pattern. Each middleware wraps the next RequestDelegate, adding behavior before and after the request flows through the pipeline.

  • Adapter β€” changes interface, doesn't add behavior
  • Proxy β€” controls access, similar wrapping structure
  • Composite β€” similar tree structure, but aggregates instead of adding behavior
  • Builder β€” can help build complex decorator chains fluently
  • SOLID β€” OCP β€” Decorator is the textbook OCP pattern