Overview
In recent posts, we identified issues that can arise with class explosion designs. Let's take a quick look at the fundamental design problems that exist.
Existing Class Hierarchy Issues
In a poorly designed hierarchy, toppings of Pizza are hard-coded at compile-time. These are actually concrete entities in the real world. If we want to add new items as toppings, we need to modify the class – a violation of the Open-Closed Principle (OCP). Ideally, we should be able to add any new topping "without modifying the class". As OCP states:
"Classes should be closed for modification and open for enhancement."
The Decorator Pattern Solution
We need the capability to add behavior to an object at runtime and provide the client of the Pizza class greater flexibility. Here is the approach to follow for maximum flexibility in design:
- Take a plain Pizza object
- Decorate it with Paneer
- Decorate it with Olives
- Call the cost() method, which will be delegated to "Add-on" or "Decorator" objects to add up their cost
How Cost Calculation Works
For a FreshVeggiePizza with Paneer and Olives, there is an important point: there must be a cost() method in each "Decorator" object as well as in the Pizza class. This is achieved by creating a super class "Decorator" for each topping item. The Food class will have cost() and getDescription() methods. Additionally, you should be able to print the description of the food according to what toppings are added, rather than hardcoding the description into the Pizza class.
Class Design With Decorator Pattern
Recipe of FreshVeggiePizza with Paneer and Olive
- Create a classic FreshVeggiePizza
- Put a layer of Paneer over it
- Spread some olives over the pizza
Your FreshVeggiePizza with paneer and olives is ready. When you print the price of the pizza, it will calculate: cost of FreshVeggiePizza + cost of Paneer + cost of Olive. This provides a fair level of dynamism in object decoration.
Open-Closed Principle Achieved
We don't need to change our Pizza class at all to add toppings that may change the cost. We don't care if the cost of paneer or olive changes, because these attributes are parameterized and "decorated" over pizzas rather than tightly coupled into the pizza class.
Decorators In Practice
To see real-life decorators, look at java.io classes:
- Replace Food with InputStream
- Replace Decorator with FilterInputStream
- Replace FreshVeggiePizza with FileInputStream/ByteArrayInputStream/StringBufferInputStream
- Replace Paneer with PushbackInputStream/BufferedInputStream/DataInputStream
Summary
The key steps to implement the Decorator Pattern:
- Identify properties and methods common to decorators and client components (in our case, the Pizza is the client)
- Create a super class for these common methods
- Create a Decorator super-class and identify methods which need to be implemented by each decorator
- Pass a reference of the client in each decorator to allow the decorator to add its own properties
- The job of the decorator is to extend the behavior of the client without modifying existing code
This pattern achieves flexibility and adherence to the Open-Closed Principle while maintaining clean, maintainable code.