Understanding the Decorator Design Pattern in C#

The Decorator design pattern is a structural pattern that allows behavior to be added to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class. It provides a flexible alternative to subclassing for extending functionality. The Decorator pattern involves a set of decorator classes that are used to wrap concrete components.
Understanding the Facade Design Pattern in C#
Example Without the Decorator Pattern
Let's consider a simple example of a coffee shop where we have different types of beverages, such as Espresso and Latte, and different add-ons like milk and sugar. In a non-pattern approach, we might create subclasses for each combination of beverages and add-ons.
using System;
namespace WithoutDecoratorPattern
{
// Base class
abstract class Beverage
{
public abstract string GetDescription();
public abstract double GetCost();
}
// Concrete classes
class Espresso : Beverage
{
public override string GetDescription() => "Espresso";
public override double GetCost() => 1.99;
}
class Latte : Beverage
{
public override string GetDescription() => "Latte";
public override double GetCost() => 2.99;
}
class EspressoWithMilk : Espresso
{
public override string GetDescription() => base.GetDescription() + ", Milk";
public override double GetCost() => base.GetCost() + 0.50;
}
class LatteWithSugar : Latte
{
public override string GetDescription() => base.GetDescription() + ", Sugar";
public override double GetCost() => base.GetCost() + 0.20;
}
// Client
class Program
{
static void Main(string[] args)
{
Beverage beverage = new EspressoWithMilk();
Console.WriteLine($"{beverage.GetDescription()} costs {beverage.GetCost()}");
Beverage anotherBeverage = new LatteWithSugar();
Console.WriteLine($"{anotherBeverage.GetDescription()} costs {anotherBeverage.GetCost()}");
}
}
}
Problems in the Non-Pattern Approach
Class Explosion: Creating a new subclass for each combination of beverage and add-on can lead to a large number of classes.
Inflexibility: It is hard to mix and match features dynamically at runtime.
Maintenance Difficulty: Adding new add-ons requires creating new subclasses, leading to increased maintenance.
How the Decorator Pattern Solves These Problems
The Decorator pattern provides a more flexible and modular approach by using composition instead of inheritance. Decorators are objects that are used to wrap concrete components, adding responsibilities dynamically.
Revisited Code with Decorator Pattern
Let's implement the Decorator pattern using a base Beverage
class and concrete decorator classes for add-ons like Milk
and Sugar
.
using System;
namespace DecoratorPattern
{
// Component
abstract class Beverage
{
public abstract string GetDescription();
public abstract double GetCost();
}
// Concrete Component
class Espresso : Beverage
{
public override string GetDescription() => "Espresso";
public override double GetCost() => 1.99;
}
class Latte : Beverage
{
public override string GetDescription() => "Latte";
public override double GetCost() => 2.99;
}
// Decorator
abstract class BeverageDecorator : Beverage
{
protected Beverage _beverage;
public BeverageDecorator(Beverage beverage)
{
_beverage = beverage;
}
}
// Concrete Decorators
class Milk : BeverageDecorator
{
public Milk(Beverage beverage) : base(beverage) { }
public override string GetDescription() => _beverage.GetDescription() + ", Milk";
public override double GetCost() => _beverage.GetCost() + 0.50;
}
class Sugar : BeverageDecorator
{
public Sugar(Beverage beverage) : base(beverage) { }
public override string GetDescription() => _beverage.GetDescription() + ", Sugar";
public override double GetCost() => _beverage.GetCost() + 0.20;
}
// Client
class Program
{
static void Main(string[] args)
{
Beverage beverage = new Espresso();
Console.WriteLine($"{beverage.GetDescription()} costs {beverage.GetCost()}");
beverage = new Milk(beverage);
Console.WriteLine($"{beverage.GetDescription()} costs {beverage.GetCost()}");
beverage = new Sugar(beverage);
Console.WriteLine($"{beverage.GetDescription()} costs {beverage.GetCost()}");
}
}
}
Benefits of the Decorator Pattern
Flexible Composition: The Decorator pattern allows for flexible combinations of behaviors by dynamically adding responsibilities to objects.
Reduced Class Explosion: Instead of creating many subclasses, the pattern uses composition to add behavior.
Extensibility: New functionalities can be added easily by creating new decorator classes.
Why Can't We Use Other Design Patterns Instead?
Adapter Pattern: The Adapter pattern is used to make incompatible interfaces compatible, rather than adding responsibilities.
Composite Pattern: The Composite pattern is used to treat individual objects and compositions of objects uniformly. It doesn't dynamically add behavior.
Proxy Pattern: The Proxy pattern controls access to an object and may add behavior, but it doesn't allow for the flexible addition of multiple behaviors.
Steps to Identify Use Cases for the Decorator Pattern
Dynamic Responsibilities: Use the Decorator pattern when you need to add responsibilities to individual objects dynamically and transparently.
Avoiding Subclass Explosion: If subclassing would lead to an impractical number of subclasses to support every possible combination, consider the Decorator pattern.
Open for Extension, Closed for Modification: When you want to adhere to the Open/Closed Principle, where classes are open for extension but closed for modification, the Decorator pattern allows adding new functionality without modifying existing code.
The Decorator design pattern provides a powerful and flexible way to add functionality to objects dynamically. It helps to keep the codebase clean and maintainable by avoiding subclass explosion and enabling easy extensions of functionality.