π Decorator Design Pattern in Java β Explained with Pizza & Toppings
Writing clean, extensible, and maintainable object-oriented code is a challenge many developers face. One elegant solution for adding new behaviors without modifying existing code is the Decorator Design Pattern.
In this blog, we'll dive deep into the Decorator pattern using one of the most delicious real-world analogies: Pizza and Toppings!
π Table of Contents
- What is the Decorator Pattern?
- Real World Analogy
- Problem Statement
- Decorator Pattern Structure
-
Java Implementation (Pizza Example)
- Step 1: Component Interface
- Step 2: Concrete Component
- Step 3: Abstract Decorator
- Step 4: Concrete Decorators
- Step 5: Client Code
π§ What is the Decorator Pattern?
The Decorator Design Pattern is a structural design pattern that lets you dynamically attach new behaviors or responsibilities to an object at runtime without altering its structure.
This is achieved by wrapping the original object inside a new object (called a decorator) that adds the new behavior.
β It follows the Open/Closed Principle β you can extend functionality without modifying existing code.
π΄ Real World Analogy
Imagine you run a pizza shop.
You sell a base pizza, and customers can choose to add toppings like:
- Extra cheese
- Olives
- Mushrooms
- JalapeΓ±os
Creating a separate class for each topping combination like PizzaWithCheeseAndOlives
would be chaotic. Instead, what if you could dynamically wrap a pizza with different toppings?
Thatβs exactly what the Decorator Pattern allows you to do.
π¨ Problem Statement
Without decorators, you might end up with many subclasses like:
class PizzaWithCheese {}
class PizzaWithCheeseAndOlives {}
class PizzaWithMushroomsAndCheeseAndOlives {}
// ... and so on
This quickly becomes unmanageable as combinations grow.
π§± Decorator Pattern Structure
Role | Responsibility | Example |
---|---|---|
Component | Interface or abstract class | Pizza |
ConcreteComponent | Real object implementing Component | MargheritaPizza |
Decorator | Abstract class implementing Component | PizzaDecorator |
ConcreteDecorator | Adds additional behavior |
CheeseTopping , OliveTopping
|
β Java Implementation (Pizza Example)
1οΈβ£ Component Interface
public interface Pizza {
String getDescription();
double getCost();
}
2οΈβ£ Concrete Component
public class MargheritaPizza implements Pizza {
@Override
public String getDescription() {
return "Margherita Pizza";
}
@Override
public double getCost() {
return 200.0;
}
}
3οΈβ£ Abstract Decorator
public abstract class PizzaDecorator implements Pizza {
protected Pizza pizza;
public PizzaDecorator(Pizza pizza) {
this.pizza = pizza;
}
public String getDescription() {
return pizza.getDescription();
}
public double getCost() {
return pizza.getCost();
}
}
4οΈβ£ Concrete Decorators
public class CheeseTopping extends PizzaDecorator {
public CheeseTopping(Pizza pizza) {
super(pizza);
}
public String getDescription() {
return pizza.getDescription() + ", Extra Cheese";
}
public double getCost() {
return pizza.getCost() + 50.0;
}
}
public class OliveTopping extends PizzaDecorator {
public OliveTopping(Pizza pizza) {
super(pizza);
}
public String getDescription() {
return pizza.getDescription() + ", Olives";
}
public double getCost() {
return pizza.getCost() + 30.0;
}
}
5οΈβ£ Client Code
public class PizzaShop {
public static void main(String[] args) {
Pizza pizza = new MargheritaPizza();
System.out.println(pizza.getDescription() + " => βΉ" + pizza.getCost());
pizza = new CheeseTopping(pizza);
pizza = new OliveTopping(pizza);
System.out.println(pizza.getDescription() + " => βΉ" + pizza.getCost());
}
}
Output:
Margherita Pizza => βΉ200.0
Margherita Pizza, Extra Cheese, Olives => βΉ280.0
π How the Decorator Pattern Works (Step by Step)
Pizza pizza = new OliveTopping(
new CheeseTopping(
new MargheritaPizza()));
Call Flow for getDescription()
:
-
OliveTopping.getDescription()
β calls-
CheeseTopping.getDescription()
β calls -
MargheritaPizza.getDescription()
β"Margherita Pizza"
- adds
", Extra Cheese"
-
adds
", Olives"
Final: "Margherita Pizza, Extra Cheese, Olives"
Call Flow for getCost()
:
- Base Pizza =
200.0
- Cheese =
+50.0
- Olives =
+30.0
- Final =
βΉ280.0
β The decorators wrap and extend the behavior dynamically.
β Advantages
- Open/Closed Principle: Add behavior without modifying existing code
- Flexible: Combine decorators in any order
- Avoids subclass explosion
- Runtime customization: Change behavior during execution
β Disadvantages
- Many small classes for each decorator
- Debugging becomes harder with deep nesting
- Order of decorators matters
π When to Use It
Use the Decorator Pattern when:
- You need to add optional features or behaviors
- You want to avoid large class hierarchies
- You need dynamic behavior composition at runtime
π§ͺ Real World Examples in Java API
-
java.io.InputStream
,OutputStream
,BufferedReader
, etc. -
java.util.Collections.unmodifiableList()
β wraps a list to prevent mutation
π§ Conclusion
The Decorator Design Pattern is a flexible and powerful tool that helps you extend behavior without changing existing code. By using composition over inheritance, you keep your code modular, extensible, and maintainable.
Key Takeaway:
Decorators are like toppings on a pizza β each one adds flavor (behavior) while wrapping the same base pizza!
More Details:
Get all articles related to system design
Hastag: SystemDesignWithZeeshanAli
Git: https://github.com/ZeeshanAli-0704/SystemDesignWithZeeshanAli
π Explore More Design Patterns in Java
- π Mastering the Singleton Design Pattern in Java β A Complete Guide
- β οΈ Why You Should Avoid Singleton Pattern in Modern Java Projects
- π Factory Design Pattern in Java β A Complete Guide
- π§° Abstract Factory Design Pattern in Java β Complete Guide with Examples
- π§± Builder Design Pattern in Java β A Complete Guide
- π Observer Design Pattern in Java β Complete Guide
- π Adapter Design Pattern in Java β A Complete Guide
- π Iterator Design Pattern in Java β Complete Guide
Top comments (0)