Skip to main content

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​

Decorator Design Pattern

UML for Decorator Design Pattern

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:

  1. PizzaDecorator isA Pizza: This signifies inheritance, indicating that PizzaDecorator will have access to all the implementations of the Pizza interface.

  2. PizzaDecorator hasA Pizza: This signifies association, which means that Pizza can be modified.

The CheeseDecorator and PepperoniDecorator are the classes that modify the behavior of Pizza without creating numerous subclasses.

Code Implementation​

Pizza Interface​

Pizza.java
public interface Pizza {
String getDescription();
double getCost();
}

BasicPizza Class​

BasicPizza.java
public class BasicPizza implements Pizza {
@Override
public String getDescription() {
return "Basic Pizza";
}

@Override
public double getCost() {
return 10.0;
}
}

PizzaDecorator Abstract Class​

PizzaDecorator.java
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​

CheeseDecorator.java
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)​

Main.java
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.