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
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β
| Approach | Classes needed for 3 coffees + 4 toppings |
|---|---|
| Inheritance (every combo) | 3 Γ 2β΄ = 48 classes |
| Decorator | 3 coffees + 4 decorators = 7 classes |
.NET Real-World Usageβ
Streamdecorators βBufferedStream,GZipStream,CryptoStreamwrap other streamsHttpClienthandlers βDelegatingHandlerchain in ASP.NET Core- ASP.NET Core Middleware β each middleware wraps the next
IAsyncEnumerablewithConfigureAwaitand 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.
Related Topicsβ
- 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