Decorator pattern is one of the most important and widely used design pattern. It helps the user to add new functionalities without altering the existing code structure.
Lets review the basic class diagram for this pattern
You are working on new project for yourself/employer or someone else. You developed a new feature and released it in production. You made sure that each feature works well and tested them properly. But somehow requirements changed after some time and you came back to existing classes to enhance them with new features. But wait! Changing existing classes will now violate one of the SOLID design, open-closed principle. How do you add new feature without modifying existing classes. Here decorator pattern will comes to rescue. Decorator pattern helps you in decorating your existing classes with new features.
Lets go through an example of making a tea
We need to make a simple class which will output cost of making a tea and description of tea. Sounds Pretty easy. Lets write the code and define the interface.
public interface ITea {
double GetCost();
string GetDescription();
}
Lets make a Tea class implementing this interface
public class Tea : ITea {
public double GetCost() {
return 3.0;
}
public string GetDescription() {
return "Simple Tea";
}
}
When we make an object of the Tea class and call the GetCost() and GetDescription() it works fine. Cool. We made a new feature.
Now after some time business requirement changed and business want to charge additional amount to consumer for extra milk or extra sugar in tea. What can be done? Obviously we can modify the above class and pass some parameters and produce response using if-else statements.
Should we do it? The clear answer is “NO”. Why? It violates open-closed principle. In real production system, changing already tested features and classes, can cause regression and it will unnecessarily put lot of pressure on our testers. So, What’s the solution? Lets introduce another abstract class called TeaDecorator which inherits from the same component as our already existing classes.
public abstract class TeaDecorator : ITea {
private readonly ITea tea;
public TeaDecorator(ITea tea) {
this.tea = tea;
}
public virtual double GetCost() {
var cost = tea.GetCost();
return cost;
}
public virtual string GetDescription() {
return tea.GetDescription();
}
}
The above class TeaDecorator implements ITea interface similar to our existing Tea class. It also contains a reference of ITea in the same class so its subclasses can hold reference of the class which need to be decorated/enhanced.
As mentioned above, our requirements have now changed and business needs to charge money for extra milk and extra sugar in addition to existing cost. So lets introduce two new classes called extra milk and extra sugar.
Please note how these new concrete decorator classes inherits from the above abstract TeaDecorator class and accepts a type of ITea instance in their constructor.
Below class charges 0.75 cents extra for additional milk in addition to existing tea cost
public class WithExtraMilk : TeaDecorator {
public WithExtraMilk(ITea t) : base(t) {
}
public override double GetCost() {
return base.GetCost() + 0.75;
}
public override string GetDescription() {
return base.GetDescription() + " with extra milk";
}
}
Below class charges 0.50 cents extra for additional sugar in addition to existing tea cost
public class WithExtraSugar : TeaDecorator {
public WithExtraSugar(ITea t) : base(t) {
}
public override double GetCost() {
return base.GetCost() + 0.5;
}
public override string GetDescription() {
return base.GetDescription() + " with extra sugar";
}
}
Wow! Our new implementation is almost ready with new features. We have implemented new features without violating open closed principle. So the above decorator classes provided a wrapper around already existing classes and helps us in avoiding to change existing structure altogether.
Finally, our client can consume these new classes with the below code
ITea tea = new Tea();
// Make a simple tea
Console.WriteLine(Environment.NewLine);
Console.WriteLine(tea.GetDescription() + " is " + tea.GetCost());
// Decorate the tea with extra milk
tea = new WithExtraMilk(tea);
Console.WriteLine(tea.GetDescription() + " is " + tea.GetCost());
tea = new WithExtraSugar(tea);
Console.WriteLine(tea.GetDescription() + " is " + tea.GetCost());
As you can see, we created a tea object and passed it into our new decorator classes to provide decoration to our already existing class objects.
Hope this article helped you to understand decorator pattern clearly.
You can find the whole code for decorator pattern here