Decorator Design Pattern

Nivedita KumariNivedita Kumari
4 min read

The Decorator Design Pattern is a structural design pattern that allows you to dynamically add new functionalities to objects without modifying their existing structure. This is done by wrapping the original object inside a decorator class, which extends its behaviour.

Key Components

  1. Component Interface

    This is an interface or abstract class that defines the structure for both basic objects and decorators

  2. Concrete Component

    This is the original object that we want to extend with new features.

  3. Decorator (Abstract Class)

    The decorator class wraps around an object and extends its behaviour without modifying the original class.

  4. Concrete Decorators

    Each concrete decorator extends Decorator and adds new functionality.

Example:

Component Interface

package DesignPatterns.structural.decorator.pizza;

public interface PizzaBase {
    public String getDescription();
    public double getCost();
}

Concrete Component

package DesignPatterns.structural.decorator.pizza;

public class MargheritaPizza implements PizzaBase{
    @Override
    public String getDescription() {
        return "Margherita Pizza";
    }

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

Decorator(Abstract Class)

package DesignPatterns.structural.decorator.pizza;

abstract public class PizzaDecorator implements  PizzaBase{
    PizzaBase pizzaBase;

    PizzaDecorator(PizzaBase pizzaBase) {
        this.pizzaBase = pizzaBase;
    }

    @Override
    public String getDescription() {
        return pizzaBase.getDescription();
    }

    @Override
    public double getCost() {
        return pizzaBase.getCost();
    }
}

Concrete Decorators

package DesignPatterns.structural.decorator.pizza;

public class CheeseDecorator extends PizzaDecorator{

    CheeseDecorator(PizzaBase pizzaBase) {
        super(pizzaBase);
    }
    @Override
    public String getDescription() {
        return super.getDescription() + "Extra Cheese";

    }

    @Override
    public double getCost() {
        return super.getCost() + 50;
    }
}
package DesignPatterns.structural.decorator.pizza;

public class PaneerDecorator extends   PizzaDecorator{
    PaneerDecorator(PizzaBase pizzaBase) {
        super(pizzaBase);
    }

    @Override
    public String getDescription() {
        return super.getDescription()+ " Paneer";
    }

    @Override
    public double getCost() {
        return super.getCost() + 60;
    }
}
package DesignPatterns.structural.decorator.pizza;

public class ChickenTikkaDecorator extends PizzaDecorator{

    ChickenTikkaDecorator(PizzaBase pizzaBase) {
        super(pizzaBase);
    }

    @Override
    public String getDescription() {
        return super.getDescription() + " Chicken Tikka";
    }

    @Override
    public double getCost() {
        return super.getCost() + 90;
    }
}
package DesignPatterns.structural.decorator.pizza;

public class Client {
    public static void main(String[] args) {
        // first order Paneer Pizza
        PizzaBase pizza = new MargheritaPizza();
        System.out.println(pizza.getDescription() + " → Cost: ₹" + pizza.getCost());

        pizza = new CheeseDecorator(pizza);
        System.out.println(pizza.getDescription() + " → Cost: ₹" + pizza.getCost());

        pizza = new PaneerDecorator(pizza);
        System.out.println(pizza.getDescription() + " → Cost: ₹" + pizza.getCost());


        //second order Chicken Tikka Pizza
        PizzaBase pizza2 = new MargheritaPizza();
        System.out.println(pizza2.getDescription() + " → Cost: ₹" + pizza2.getCost());


        pizza2 = new CheeseDecorator(pizza2);
        System.out.println(pizza2.getDescription() + " → Cost: ₹" + pizza2.getCost());

        pizza2 = new ChickenTikkaDecorator(pizza2);
        System.out.println(pizza2.getDescription() + " → Cost: ₹" + pizza2.getCost());




    }
}

Why Use the Decorator Pattern?

Flexibility – You can dynamically add toppings (features) without modifying the PizzaBase class.
Open/Closed Principle – You don’t need to modify the base pizza class to add new toppings.
Avoids Large Inheritance Trees – Instead of creating separate subclasses like CheesePizza, CheesePaneerPizza, etc., we use decorators to add toppings dynamically.

When to Use the Decorator Pattern?

  1. Use when you need to dynamically add features to an object.

  2. Use when subclassing would create too many classes.

  3. Use when behavior needs to be added selectively at runtime.

  4. Avoid if your class structure is simple and doesn’t need dynamic changes.

  5. Avoid if debugging complexity is a concern in your project.

Pros (Advantages)

1. Adds Behavior Dynamically

  • You can add features (like extra cheese, olives, etc.) to an object at runtime without modifying the base class.

  • Example: A customer orders a pizza, and you can customize it dynamically with toppings.

2. Follows Open/Closed Principle

  • The base class (PizzaBase) remains unchanged, and new functionalities (decorators) can be added independently.

  • No need to modify existing code to add new features.

3. Avoids Large Inheritance Trees

  • Instead of creating multiple subclasses like CheesePizza, CheesePaneerPizza, etc., decorators allow us to mix and match functionalities without subclass explosion.

4. More Flexible Than Subclassing

  • You can combine multiple decorators in different ways, leading to a modular and reusable approach.

  • Example: A MargheritaPizza can have just cheese, cheese + paneer, or cheese + paneer + chicken.

5. Allows Single Responsibility Principle (SRP)

  • Each decorator class focuses on one responsibility (e.g., adding cheese, adding olives) rather than modifying a single, monolithic class.

Cons (Disadvantages)

1. Increases Complexity

  • Wrapping multiple decorators creates a lot of small classes, making code harder to read and debug.

  • Example: If you have 10 different toppings, you end up with 10 decorator classes.

2. Harder to Debug and Maintain

  • Since decorators are nested, debugging can be tricky, especially when multiple decorators modify the same method.

  • Example: If the final pizza cost is incorrect, you may need to check multiple layers of decorators.

3. Order of Application Matters

  • Applying decorators in a different order may lead to different results, making behavior unpredictable in some cases.

  • Example: If CheeseDecorator adds a melted cheese effect, and ChickenTikkaDecorator adds dry chicken, applying them in the wrong order might change the expected outcome.

4. Can Lead to Performance Overhead

  • Every decorator wraps another object, leading to more method calls and object creation at runtime.

  • In cases where performance is critical, the overhead of multiple objects might be an issue.

0
Subscribe to my newsletter

Read articles from Nivedita Kumari directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nivedita Kumari
Nivedita Kumari

I am wokring as SDE-1 at BetterPlace