Consider a scenario: We have a coffee class with various types(espresso, lattes, cappuccino), and we want to add extra ingredients such as milk and sugar to these types of coffee.
Example “Coffee“ Class
//Coffee.cs
public class Coffee
{
public string Type { get; set; }
public bool HasMilk { get; set; }
public bool HasSugar { get; set; }
public bool HasSyrup { get; set; }
public Coffee(string type, bool hasMilk, bool hasSugar, bool hasSyrup)
{
Type = type;
HasMilk = hasMilk;
HasSugar = hasSugar;
HasSyrup = hasSyrup;
}
public void Prepare()
{
Console.WriteLine($"Preparing {Type} coffee... " );
if (HasMilk)
{
Console.WriteLine("Adding milk...");
}
if (HasSugar)
{
Console.WriteLine("Adding sugar... " );
}
if (HasSyrup)
{
Console.WriteLine("Adding syrup... " ) ;
}
Console.WriteLine("Coffee is ready!");
}
}
Let's consider a scenario where we want to create variations of coffee with different combinations of milk, sugar, and syrup. Here is what it will look like. We are using inheritance, and we create subclasses for each combination of coffee type and extra ingredient.
//Coffee Without Decorator.cs
public class CoffeeWithMilk: Coffee
{
public CoffeeWithMilk(string type) : base(type, true, false, false)
{
}
}
public class CoffeeWithSugar: Coffee
{
public CoffeeWithSugar(string type) : base(type, false, true, false)
{
}
}
public class CoffeeWithMilkAndSugar: Coffee
{
public CoffeeWithMilkAndSugar(string type) : base(type, true, true, false)
{
}
}
Problem
- Code Duplication: Creating variations of coffee leads to duplicating code, such as creating classes like MilkCoffee or DoubleSugarCoffee, resulting in maintenance overhead.
- Limited Extensibility: Adding new customization options like whipped cream or different syrups requires modifying the Coffee class directly, violating the Open/Closed principle of SOLID design.
- Tight Coupling: The coffee class is tightly coupled with specific customization options like milk, sugar, and syrup, making it difficult to change these options independently without modifying the coffee class.
- Violation of Single Responsibility Principle: The Coffee class is responsible for representing coffee and managing its customization options. It violates the Single Responsibility Principle, which advocates for classes having only one reason to change.
- Maintenance Challenges: As customization options increase, maintaining and understanding the Coffee class becomes more challenging, making it harder to predict the behavior of the class as more options are added.
How can we solve the above problems?
Decorator Pattern
- Decorator Pattern it’s comes under the category of structural design patterns.
- “Classically, Decorators offered the ability to add behavior to existing classes in a system dynamically.“
Main components of the Decorator Pattern
- Component Interface: This is the interface that defines the common interface for all objects that can be decorated. It typically declares operations that the concrete components and decorators can perform.
- Concrete Component: This is the base class that implements the Component interface. It represents the core functionality that can be optionally extended by decorators.
- Decorator: This is an abstract class that implements the Component interface and holds a reference to an instance of the Component interface. It serves as the base class for all decorators. Decorators typically enhance or modify the behavior of the components they decorate.
- Concrete Decorators: These are the classes that extend the Decorator class and add specific behaviors or responsibilities to the component. They wrap the concrete component and delegate calls to it while also adding their behavior.
Let’s understand by modifying the above code structure.
Create a common interface for all coffee components, which contains a single method, Prepare().
//ICoffee.cs
public interface ICoffee
{
void Prepare();
}
SimpleCoffee Class
CoffeeDecorator Abstract Class
MilkDecorator, SugarDecorator, SyrupDecorator Classes
Key Takeaways: The Decorator Pattern
Decorator Pattern offers a flexible and scalable solution for extending the behavior of objects dynamically at runtime. "Always use composition over inheritance" is a principle in object-oriented programming design. It suggests that favoring composition, where objects contain instances of other objects, is often more flexible, maintainable, and scalable than relying solely on inheritance hierarchies. Decorator Pattern helps us to achieve that.
Hope you got a clear understanding of how to implement the decorator pattern in your existing code to make it more flexible and understandable.