Decorator Pattern — What It Is And How To Use It
Last year, I was on a business trip to Mumbai, I was feeling hungry and wanted to order delicious and expensive food 😜. And I was craving for pizza so, ordered Dominos cheese burst pizza along with loads of extra toppings. Of course, it was just as mouth-watering as the above picture! Today, we’ll learn about the Decorator design pattern with an example of pizza. Suppose we are developing a billing software for the dominos. Customers can order their favorite pizza with extra toppings on it. We have different types of base, a variety of toppings that add up to the actual cost of the pizza. How can we build such kind of system such that it requires minimal modification yet we can add new functionality in the future? Before digging into the decorator pattern, let’s learn a design principle.
The open-closed principle — A system should be closed to modification and open for extension.
Let’s explore a little bit here. Let’s say, we have a Person class with firstName as a class variable.
class Person {
String firstName;
Person(String firstName){
this.firstName = firstName;
}
}
Now as a change in requirement, we might want to add lastName as a class variable. For this example, we can definitely do it; however, we can’t always do that. Because it might be possible that this person’s class might be used by multiple other teams. We may end up in a situation where a little change can break the existing code of the system. Hence, we should always stress on building systems that are closed for modification, but they should be open for extension, like adding new functionality on top of existing ones.
Okay, coming back to our pizza requirement, we have different types of pizza bases, different types of toppings how can we approach it. Here are a few methods.
- Create a class for Pizza and each type of pizza inheriting the base class. Wait! What about toppings? How are we going to add the cost of toppings to the pizza? Hmm, we can create classes like MargaritaWithExtraCheese, PeppyPaneerWithExtraPaneerCheeseOlive, FarmHouseWithExtraVegCheeseOlivePaneer. Pretty small class names, right! As you can see this is wort possible design. We have to create classes for every possible scenario. This is ridiculous.
- Why not create instance variables for each type of toppings in the pizza base class! This can definitely save us from the class explosion. Like this.
We can now check if extra chess is present, then we can add the extra cost of cheese to the pizza. This is definitely a better strategy compared to the previous one! But wait, there are problems here.
- What if the price changes of any toppings? We’ll have to alter the code.
- New toppings will force us for the modification of the code in the main class.
This is not a good design as we are violating the open-closed principle.
The decorator pattern comes to the rescue! We’ll start with our pizza base class and add all the toppings at runtime. Here is the procedure of the decorator pattern.
- Start with a pizza base object.
- Decorate the pizza with toppings like cheese, paneer, or olive. We will wrap the pizza base object with the decorator. This will make sense once we look at the code.
- Call the getPrice() method and rely on a delegation to get the total cost of the pizza. To simplify it, we’ll call the getPrice() method from the outermost object. The outermost object wraps another decorator object so it will call the getPrice() method on the object that it wraps. Each layer adds the price of itself and then delegate the cost calculation to the inner object that it wraps. In this way, the price of the pizza will be calculated.
Hence in this way, decorators (in our case toppings) can be added at the runtime with as many decorators as we like. Cool, now without wasting more time, let’s jump straight to the code!
Let’s add the Pizza class that will be abstract or base class for all of the other classes. It can be an interface too. We’ve also implemented a concrete pizza — FarmHouse.
Now let’s define our toppings. Code is pretty self-explanatory.
Let’s see how we can get our favorite pizza and calculate its cost. We have olive as the outermost object, followed by paneer and then base pizza at the center of the circle.
The getPrice() method will call the getPrice method defined in the Olive class. Followed by Paneer and Pizza.
Total Price = 40 (Price of extra olive) + 50 (Price of extra paneer) + Cost of base pizza.
Phew! That’s the end of the decorator pattern. No wait, let’s look at the definition of the pattern in the Head First Design Pattern book.
The decorator pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending the functionality.
Thank you for reading till now,
Happy Coding!