Lesson 3
Exploring the Decorator Pattern in C#
Exploring the Decorator Pattern

Having learned about the Adapter and Composite Patterns, you're well on your way to mastering structural design patterns in C#. In this lesson, we'll dive into the Decorator Pattern, another important structural pattern that allows us to add new functionalities to objects dynamically and transparently. This is particularly useful when you want to enhance the behavior of objects without modifying their code.

The Decorator Pattern enables you to wrap an object with additional behavior in a flexible and reusable way. You'll learn how to use this pattern to extend the functionality of objects in a clean and maintainable manner.

Understanding the Decorator Pattern

To understand the Decorator Pattern, let's think about a coffee shop where you can customize your coffee with various add-ons. Consider starting with a simple cup of coffee and then enhancing it with ingredients like milk, sugar, or whipped cream.

Simple Coffee (Core Component): Think of the basic coffee as the core component. It has fundamental properties, such as description and cost.

Milk (Decorator): Adding milk to the coffee decorates it with extra description ("Milk") and additional cost.

Sugar (Decorator): Similarly, adding sugar will decorate the coffee with a sugar description and additional cost.

Multiple Decorations

You can layer decorators. For instance, you can first add milk and then sugar to the already milk-decorated coffee. Each decorator wraps the core component or another decorator, adding its behavior.

C#
1Coffee simpleCoffee = new SimpleCoffee(); 2Coffee milkCoffee = new MilkDecorator(simpleCoffee); 3Coffee milkAndSugarCoffee = new SugarDecorator(milkCoffee);

In this structure, each decorator extends the functionality of the original component dynamically. You start with a basic coffee and, by wrapping it with different decorators, you can create complex coffee orders.

Benefits of the Decorator Pattern

The Decorator Pattern allows you to dynamically add or remove functionality to objects without altering their structure. This leads to more modular and maintainable code compared to subclassing for every possible combination of enhancements. In our coffee shop example, you can easily extend the coffee's behavior at runtime by adding decorators like milk, sugar, or any other ingredients.

Step 1: Creating the Coffee Class

We start by defining an abstract Coffee class. This class includes a GetDescription method and a Cost method that will be overridden by subclasses. The SimpleCoffee class extends Coffee and provides the implementation for the Cost method.

C#
1public abstract class Coffee 2{ 3 public virtual string GetDescription() 4 { 5 return "Coffee"; 6 } 7 8 public abstract double Cost(); 9} 10 11public class SimpleCoffee : Coffee 12{ 13 public override double Cost() 14 { 15 return 2.0; 16 } 17}
Step 2: Creating the CoffeeDecorator Base Class

Next, we create a CoffeeDecorator class that also extends Coffee. This class takes an instance of Coffee and delegates calls to GetDescription and Cost methods to the wrapped instance.

C#
1public abstract class CoffeeDecorator : Coffee 2{ 3 protected Coffee decoratedCoffee; 4 5 public CoffeeDecorator(Coffee coffee) 6 { 7 this.decoratedCoffee = coffee; 8 } 9 10 public override string GetDescription() 11 { 12 return decoratedCoffee.GetDescription(); 13 } 14}
Step 3: Adding MilkDecorator

Following this, we extend CoffeeDecorator to create a MilkDecorator class. It overrides the GetDescription and Cost methods to add the description and cost of milk to the original coffee.

C#
1public class MilkDecorator : CoffeeDecorator 2{ 3 public MilkDecorator(Coffee coffee) : base(coffee) { } 4 5 public override string GetDescription() 6 { 7 return decoratedCoffee.GetDescription() + ", Milk"; 8 } 9 10 public override double Cost() 11 { 12 return decoratedCoffee.Cost() + 0.5; 13 } 14}
Step 4: Adding SugarDecorator

Similarly, we create a SugarDecorator class by extending CoffeeDecorator. This decorator adds sugar-related description and cost.

C#
1public class SugarDecorator : CoffeeDecorator 2{ 3 public SugarDecorator(Coffee coffee) : base(coffee) { } 4 5 public override string GetDescription() 6 { 7 return decoratedCoffee.GetDescription() + ", Sugar"; 8 } 9 10 public override double Cost() 11 { 12 return decoratedCoffee.Cost() + 0.2; 13 } 14}
Applying the Decorator Pattern

Now, let's see how these components work together in the main method:

  1. Creating a SimpleCoffee instance: We start by creating a basic SimpleCoffee object. This represents our core component with the base functionality.

    C#
    1Coffee myCoffee = new SimpleCoffee(); 2Console.WriteLine($"{myCoffee.GetDescription()} ${myCoffee.Cost()}"); 3// Output: Coffee $2.0
  2. Decorating with Milk: Next, we wrap (decorate) the SimpleCoffee object with a MilkDecorator. This adds the milk description and cost to the original coffee.

    C#
    1myCoffee = new MilkDecorator(myCoffee); 2Console.WriteLine($"{myCoffee.GetDescription()} ${myCoffee.Cost()}"); 3// Output: Coffee, Milk $2.5
  3. Decorating with Sugar: Finally, we wrap the milk-decorated coffee with a SugarDecorator. This adds the sugar description and cost to the already milk-enhanced coffee.

    C#
    1myCoffee = new SugarDecorator(myCoffee); 2Console.WriteLine($"{myCoffee.GetDescription()} ${myCoffee.Cost()}"); 3// Output: Coffee, Milk, Sugar $2.7

This demonstrates how you can start with a base component (SimpleCoffee) and dynamically add functionalities by layering decorators. Each decorator adds its own behavior while preserving the core functionality of the base component.

Conclusion

Mastering the Decorator Pattern is important because it allows you to extend the functionality of objects at runtime without altering their structure. This leads to more flexible and maintainable code compared to using subclassing for every new feature. In our coffee example, you can easily add new features like milk and sugar by layering decorators, which can be combined in various ways. Whether you need to add functionality to user interface components, logging systems, or graphical rendering engines, the Decorator Pattern provides a powerful and elegant solution.

Let's get started with the practice section, where you'll implement and extend the Decorator Pattern step-by-step.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.