🎨 Decorator Design Pattern: Add Behavior Without Changing Code

🎨 Decorator Design Pattern: Add Behavior Without Changing Code

In the world of scalable and maintainable software, modifying existing code to add new functionality can introduce bugs, break behavior, or violate open/closed principles. That’s where the Decorator Design Pattern shines.


🔍 What Is the Decorator Pattern?

The Decorator Pattern is a Structural Design Pattern that allows you to dynamically attach new responsibilities or behavior to an object without altering its structure.

Instead of subclassing to add behavior, you wrap objects in other decorator objects that extend functionality.


🧠 Real-World Analogy

Think of a coffee order ☕.

  • You start with a base coffee.
  • Then you add milk, sugar, caramel, or even whipped cream—each decorator enhances the original coffee without changing the coffee class itself.

Similarly in software:

  • You wrap a core object with decorators to add features at runtime.


🧑💻 Real-World Use Cases in Software

  1. UI Components (Java, JavaScript, React)
  2. Logging and Monitoring
  3. Java I/O Streams
  4. Middleware in Express.js (Node.js)


// Base.java 

package LLD.DecoratorDesign;
interface Coffee{
    double getCost();
    String getDetail();
}

// Base class on which we add responsibilty dynamically in runtime
class BaseCoffee implements Coffee{
    public double getCost(){
        return 10.0;
    }
    public String getDetail(){
        return "Base Coffee";
    }

}        
// Decorator.java --> Abstract code of Decorator
package LLD.DecoratorDesign;
public abstract class Decorator implements Coffee{
    private Coffee decoratedCoffee;
    Decorator(Coffee coffee){
        this.decoratedCoffee=coffee;
    }
    public double getCost(){
        return decoratedCoffee.getCost();
    }
    public String getDetail(){
        return decoratedCoffee.getDetail();
    }
}
// Implementation of Decorator 
class CoffeeWithMilk extends Decorator{
    CoffeeWithMilk(Coffee coffee){
        super(coffee);
    }
    public double getCost(){
        return super.getCost()+2.0;
    }
    public String getDetail(){
        return super.getDetail()+" with Milk";
    }
}
class CoffeeWithCoco extends Decorator{
    CoffeeWithCoco(Coffee coffee){
        super(coffee);
    }
    public double getCost(){
        return super.getCost()+4.0;
    }
    public String getDetail(){
        return super.getDetail()+" with Coco";
    }
}        
// Main.java

package LLD.DecoratorDesign;
public class Main{
    public static void main(String[] args){
        System.out.println("Hello World from Main.java");
        Coffee simplecoffee=new BaseCoffee();
        Coffee cappuccino=new CoffeeWithMilk(simplecoffee);
        System.out.println(cappuccino.getDetail());
        System.out.println(cappuccino.getCost());
/*
Below we wrap the cappuccino with coco features
when we create hotchoco obj by passing cappuccino it sets the 
decoratorCoffee (i.e the parent hotchoco as cappuccino) which is dynamic and at any point we can add hotchoco feature to any other 
coffee by passing that coffee to hotchoco constructor 
*/
        Coffee hotChoco=new CoffeeWithCoco(cappuccino);
        System.out.println(hotChoco.getDetail());
        System.out.println(hotChoco.getCost());
    }
}        

🧰 When to Use the Decorator Pattern

  • You want to add behavior without modifying the existing code
  • You need to combine features flexibly at runtime .
  • You follow the Open/Closed Principle – classes should be open for extension, closed for modification
  • You want to avoid deep and rigid class hierarchies


✅ Benefits

  • Flexibility in adding/removing functionality
  • Promotes composition over inheritance
  • Single Responsibility Principle: Decorators handle one concern at a time
  • Enhances code reusability and testability


⚠️ When Not to Use

  • When object creation and behavior is simple—this might introduce unnecessary layers
  • When performance is critical—decorators can add runtime overhead


🧩 Key Takeaway

The Decorator Pattern isn’t just about adding “extras.” It’s about writing extensible and maintainable code—allowing your software to evolve without breaking its core.


Great article, at first I thought why not inheritance, but certainly you can’t know everything on day one in software. It gives great power to a system designer to implement behaviour on the go.

Like
Reply

To view or add a comment, sign in

More articles by Samartha Khare

Explore content categories