Introduction
The Decorator pattern is a member of the structural design patterns. It allows dynamic addition of new functionalities to objects at runtime by encapsulating the base object within specialized objects called decorators. Decorators extend behavior of the base object without changing its structure.
Through the use of composition, each decorator dynamically adds a specific functionalities to the base object, enhancing flexibility and modularity.
Problem
Imagine a coffee bar offering two standard black coffees available in two sizes: normal and large. Customers can customize their coffee orders by adding ingredients like sugar, milk, and cream in varying amounts. How can we handle this flexibility?
The first thought might be to use inheritance to extend base coffee classes. However, inheritance is static, locking the behavior of objects at compile time. This approach demands creating numerous subclasses for all potential combinations, leading to complexity and a lack of scalability. Additionally, any future addition of new ingredients would require new classes, making this solution short-sighted.
Another, better approach is to use Decorator pattern. This pattern enables the addition of new features to existing objects dynamically at runtime. With decorators representing each ingredient, the system becomes more adaptable and modular. Adding a new ingredient in the future requires implementing only a new decorator, ensuring scalability and ease of modification.
Structure
ICoffee: Coffee interface. It declares the interface, which is implemented by both concrete components and the base decorator.
Normal coffee, Large coffee: Concrete components. They implement the ICoffee interface and provide the core functionalities of different coffee sizes.
BaseCoffeeDecorator: Abstract class, base decorator. It implements the ICoffee interface and additionally maintains a reference to the wrapped object, which can either be a base object or another decorator.
Sugar decorator etc.: Concrete decorators. They extend base decorator and add new features to the objects by wrapping them.
Implementation
//C# code
// Coffee interface
internal interface ICoffee
{
public double CalculatePrice();
}
// Concrete base coffees
internal class NormalCoffee : ICoffee
{
public double CalculatePrice()
{
return 5;
}
}
internal class LargeCoffee : ICoffee
{
public double CalculatePrice()
{
return 10;
}
}
// Base decorator
internal abstract class BaseCoffeeDecorator : ICoffee
{
private ICoffee _coffee;
protected BaseCoffeeDecorator(ICoffee coffee)
{
_coffee = coffee;
}
public virtual double CalculatePrice()
{
return _coffee.CalculatePrice();
}
}
// Concrete decorators
internal class SugarDecorator : BaseCoffeeDecorator
{
public SugarDecorator(ICoffee coffee) : base(coffee) { }
public override double CalculatePrice()
{
return base.CalculatePrice() + 2;
}
}
internal class MilkDecorator : BaseCoffeeDecorator
{
public MilkDecorator(ICoffee coffee) : base(coffee) { }
public override double CalculatePrice()
{
return base.CalculatePrice() + 3;
}
}
internal class CreamDecorator : BaseCoffeeDecorator
{
public CreamDecorator(ICoffee coffee) : base(coffee) { }
public override double CalculatePrice()
{
return base.CalculatePrice() + 5;
}
}
internal class Program
{
static void Main(string[] args)
{
// Create normal coffee object
var normalCoffee = new NormalCoffee();
Console.WriteLine($"Coffee price: {normalCoffee.CalculatePrice()}");
// Add sugar to normal coffee
var normalCoffeeWithSugar = new SugarDecorator(normalCoffee);
Console.WriteLine($"Coffee price: {normalCoffeeWithSugar.CalculatePrice()}");
// Add milk to normal coffee with sugar
var normalCoffeeWithSugarAndMilk = new MilkDecorator(normalCoffeeWithSugar);
Console.WriteLine($"Coffee price: {normalCoffeeWithSugarAndMilk.CalculatePrice()}");
}
} Result:
Coffee price: 5 PLN
Coffee price: 7 PLN
Coffee price: 10 PLN Conclusion
The client (Main method) creates a base object and then decides which functionalities should be added to this object. This enhancement of features occurs at runtime, offering enhanced flexibility. The wrapped object can be either the base object or another decorator, since both concrete components and decorators implement the ICoffee interface.
The Decorator pattern should be utilized when the objects need the ability to dynamically extend their functionalities at runtime.
Decorator pattern aligns with:
- Single Responsibility Principle: Decorators focus on enhancing specific functionalities of the objects.
- Open/Closed Principle: New features can be added by implementing new decorator classes, enhancing extensibility without requiring modifications to the existing code.

