We have come to the fourth topic of the Gamma Structural Category, it's the Decorator Design Pattern. This pattern is useful when you need to implement additional features to an existing class.
Note: Code can be downloaded in my Github.
1. What is the Decorator Design Pattern?
The Decorator Design Pattern helps developers to add new functionality to a class without modifying it's existing content or structure. This design pattern falls under the Structural Category in the GoF.
Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors.
2. When to use the Decorator Design Pattern?
As suggested above, when a system or a class needs to be enhanced by adding new functionality, it's time to consider the Decorator Design Pattern. Especially, the existing classes are not editable due to Open-Closed Design Principle or other business reasons.
Let's take an example of a Pizza restaurant's ordering system. The Pizza restaurant is selling different kinds of Pizza at a fixed price for each. When the business grows, the restaurant attracts more customers with higher expectations. Some customers may not want the regular pizzas that the restaurant is selling, they may want to add extra toppings or swap the toppings and the pizzas that the restaurant has. However, your ordering system can't calculate the price of a customized Pizza at this moment.
So, you need to add new functionality to calculate the price of a customized pizza. However, the problem is the ordering system is already there and has been used for awhile without any problems. Your project manager doesn't want to modify the existing code to avoid big impact on the system. Therefore, the Decorator Design Pattern is a good help in this case.
3. How to implement the Decorator Design Pattern?
The idea of the Decorator Design Pattern is to create a class which inherits the existing class to implement additional functions. First, take a look at the existing system that the Pizza restaurant has.
Step 1: Examine the existing classes
The systems has 3 kinds of Pizza: Pepperoni, Sausage, and Mushroom. There is an abstract Pizza class and 3 classes, which inherit the abstract class, for each kind of Pizza.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | public abstract class Pizza { protected string Name { get ; set ; } protected double Price { get ; set ; } public virtual double GetPrice() { return Price; } public override string ToString() { return $ "This { Name } pizza cost ${ Price }" ; } } public class Peperonni : Pizza { public Peperonni() { Name = "Peperonni" ; Price = 8.99; } } public class Sausage : Pizza { public Sausage() { Name = "Sausage" ; Price = 9.99; } } public class Mushroom : Pizza { public Mushroom() { Name = "Mushroom" ; Price = 6.99; } } |
Step 2: Create an abstract decorator class
Create an abstract class named ToppingsDecorator. This class is used for the new feature to add toppings to the pizza.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public abstract class ToppingsDecorator : Pizza { public Pizza BasePizza { get ; set ; } public ToppingsDecorator(Pizza pizzaToDecorate) { BasePizza = pizzaToDecorate; } public override double GetPrice() { return (BasePizza.GetPrice() + this .Price); } } |
Step 3: Add classes to implement the decorator
Add more classes for each available toppings. These classes are inherited from the ToppingsDecorator class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | public class ExtraCheeseTopping : ToppingsDecorator { public ExtraCheeseTopping(Pizza pizzaToDecorate) : base (pizzaToDecorate) { Name = $ "Extra Cheese {pizzaToDecorate.Name}" ; Price = 0.99; } } public class MushroomTopping : ToppingsDecorator { public MushroomTopping(Pizza pizzaToDecorate) : base (pizzaToDecorate) { Name = $ "Mushroom {pizzaToDecorate.Name}" ; Price = 1.49; } } public class PepperoniTopping : ToppingsDecorator { public PepperoniTopping(Pizza pizzaToDecorate) : base (pizzaToDecorate) { Name = $ "Pepperoni {pizzaToDecorate.Name}" ; Price = 1.49; } } public class SausageTopping : ToppingsDecorator { public SausageTopping(Pizza pizzaToDecorate) : base (pizzaToDecorate) { Name = $ "Sausage {pizzaToDecorate.Name}" ; Price = 1.49; } } |
Done! So now you can try this ordering system by initiating the ordered pizza and add toppings on it. Below is the code on the client showing how the system works both before and after the enhancement.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | class Program { static void Main( string [] args) { //1. The original system doesn't allow to order customized Pizza Console.WriteLine( "Ordering original Pizzas" ); Pizza pPizza = new Peperonni(); Console.WriteLine(pPizza.ToString()); Pizza sPizza = new Sausage(); Console.WriteLine(sPizza.ToString()); Pizza mPizza = new Mushroom(); Console.WriteLine(mPizza.ToString()); Console.WriteLine(); //2. After enhancing the system, can order customized pizza now Console.WriteLine( "Ordering customized Pizzas" ); Pizza myPizza = new Peperonni(); Pizza mushroom = new MushroomTopping(myPizza); Pizza extraCheaseMushroom = new ExtraCheeseTopping(mushroom); Console.WriteLine(extraCheaseMushroom.ToString()); } } |
Output:
4. Conclusion
The Decorator Design Pattern is helpful when you are enhancing your applications by adding more features without modifying the existing code. I hope this is helpful and please let me know your thoughts by commenting below! See you next time!