Decorator Design Pattern
Problem​
You: Hi Shubh, today I want to learn about the Decorator Design Pattern. I thought why not learn from you. If you don't mind, can you please explain it to me?
Shubh: Hi, definitely! I'll explain it using a classic example. Let's say we need to build a Domino's kind of application. We would definitely create a Pizza
class for that. My question to you is, what if a customer wants a pizza with cheese on it? What would we do in that case?
You: Simple, just create a CheesePizza
class that would inherit all the basic properties from the Pizza
class and have some of its own, like a different price.
Shubh: What if there's a customer who wants a pizza with pepperoni?
You: We would create a PepperoniPizza
class and do the same thing as with CheesePizza
.
Shubh: What if a customer wants a pizza with cheese and pepperoni? This could go on indefinitely. Someone might just want sausage, another might want extra cheese. Do you feel that creating classes for each request is a legitimate approach? This problem is known as Class Explosion, where the number of subclasses increases drastically due to different combinations of features.
You: Now, I see your point. But then how do we address this issue?
Shubh: The Decorator Design Pattern would solve this problem. Let's understand its UML diagram first.
UML​
Keep in mind that the above UML is tailored to our example, not a generalized one.
The need for Pizza
and its implementation BasicPizza
is straightforward. The key component that solves our issue is the PizzaDecorator
.
Notice that it has two types of relationships with the Pizza
:
-
PizzaDecorator isA Pizza
: This signifies inheritance, indicating thatPizzaDecorator
will have access to all the implementations of thePizza
interface. -
PizzaDecorator hasA Pizza
: This signifies association, which means thatPizza
can be modified.
The CheeseDecorator
and PepperoniDecorator
are the classes that modify the behavior of Pizza
without creating numerous subclasses.
Code Implementation​
Pizza Interface​
public interface Pizza {
String getDescription();
double getCost();
}
BasicPizza Class​
public class BasicPizza implements Pizza {
@Override
public String getDescription() {
return "Basic Pizza";
}
@Override
public double getCost() {
return 10.0;
}
}
PizzaDecorator Abstract Class​
public abstract class PizzaDecorator implements Pizza {
protected Pizza pizza;
public PizzaDecorator(Pizza pizza) {
this.pizza = pizza;
}
@Override
public String getDescription() {
return pizza.getDescription();
}
@Override
public double getCost() {
return pizza.getCost();
}
}
CheeseDecorator Class​
public class CheeseDecorator extends PizzaDecorator {
public CheeseDecorator(Pizza pizza) {
super(pizza);
}
@Override
public String getDescription() {
return super.getDescription() + ", Cheese";
}
@Override
public double getCost() {
return super.getCost() + 5.0;
}
}
PepperoniDecorator Class​
public class PepperoniDecorator extends PizzaDecorator {
public PepperoniDecorator(Pizza pizza) {
super(pizza);
}
@Override
public String getDescription() {
return super.getDescription() + ", Pepperoni";
}
@Override
public double getCost() {
return super.getCost() + 2.0;
}
}
Main Class (Client Code)​
public class Main {
public static void main(String[] args) {
// This is like recursion, where the base case is Basic Pizza
// (cost = 10.0 (innermost) + 5.0 (mid) + 2.0 (outermost))
Pizza pizza = new PepperoniDecorator(new CheeseDecorator(new BasicPizza()));
System.out.println(pizza.getDescription());
System.out.println("Total cost: $" + pizza.getCost());
}
}
Key Benefits of Using the Decorator Design Pattern​
Before watching, make sure you give it a try
-
Flexibility: The Decorator Pattern allows you to add behavior to objects dynamically at runtime. You can add, remove, or extend features of an object without changing the underlying code.
-
Avoids Class Explosion: Instead of creating a new class for every possible combination of features, decorators allow you to mix and match features by layering decorators around a core object.
-
Open/Closed Principle: The Decorator Pattern adheres to the Open/Closed Principle, which states that classes should be open for extension but closed for modification. You can add new functionality by creating new decorators without modifying existing code.
-
Improved Code Maintenance: It promotes cleaner code by keeping classes focused on their primary responsibilities and delegating additional responsibilities to decorators.
-
Runtime Flexibility: Different combinations of behaviors can be created at runtime, allowing for a more flexible and adaptable system.