Decorator Design Pattern

The Decorator Design Pattern adds additional functionalities dynamically to an object. A decorator extends the responsibilities of a class by using composition not inheritance.

A decorator object contains an original object instance as member variable and it adds new functionalities either before or/and after delegating it to the original object. It comes under structural design pattern as it provides one of the best ways to extend the responsibilities of a class. This pattern is useful for situations where you need to extend the functionality of classes in a flexible and reusable way.

In this tutorial, we'll explore the Decorator Design Pattern in Java, discussing its structure, implementation, best practices, and advantages.


Structure of the Decorator Design Pattern

The Decorator Design Pattern consists of the following components:
  • Component : This is the abstract class or interface that defines the interface for objects that can have responsibilities added to them dynamically.

  • Concrete Component : This is the concrete class that implements the Component interface and defines the basic behavior to which additional responsibilities can be added.

  • Decorator : This is the abstract class that also implements the Component interface and has a reference to a Component object. It maintains a reference to the component and defines an interface that conforms to the Component interface.

  • Concrete Decorator : This is the concrete class that extends the Decorator class and adds specific responsibilities or behaviors to the component.

Advantages of Decorator Design Pattern

  • Flexibility in Extending Behavior : The Decorator pattern's main benefit is in its ability to adapt and change an object's behavior dynamically. Easy customisation is possible since decorators can be added or deleted without changing the underlying components.

  • Open for Extension, Closed for Modification : The Open-Closed Principle is followed by the pattern, which permits the addition of new decorators without changing the existing code. This encourages a design that is closed to modification yet open to extension.

  • Single Responsibility Principle : By giving each decorator a distinct set of duties, the Decorator design complies with the Single Responsibility Principle. Every decorator contributes to a modular and maintainable design by concentrating on a specific facet of behavior.

  • Avoidance of Class Explosion : The Decorator pattern allows for a more modular and controllable approach than traditional subclassing, where the number of classes can explode with various combinations. It stays away from making a ton of subclasses for each potential pairing of actions.

  • Dynamic Composition of Behaviors : A variety of combinations of behaviors can be produced by dynamically combining decorators. This dynamic composition is especially useful for complicated systems that need a wide range of features that may be customized.

  • Reuse of Existing Code : This pattern permits the reuse of decorators as well as components from existing code. It is possible to reuse components in multiple contexts and to mix decorators in different ways to meet diverse needs.

  • Fine-Grained Control over Features : The behaviors and features that are introduced to objects are within the developers' precise control. Developers can customize objects to fulfill particular requirements by choosing and combining appropriate decorators.

  • Maintainability and Evolution : By offering a scalable and adaptable method of expanding behavior, the Decorator design helps to make code more maintainable. This adaptability is especially useful for systems that might change over time to accommodate new requirements.
By leveraging these advantages, the Decorator Design Pattern becomes a powerful mechanism for enhancing the behavior of objects in a flexible, reusable, and maintainable way. It is particularly useful in scenarios where there is a need to customize or extend the functionality of objects dynamically.

When we should use Decorator Pattern

  • When we want to dynamically add or remove functionalities to a class without affecting the behaviour of other objects from the same class.
  • When we want to decouple Concrete implementations from responsibilities and behaviours.
  • We want to add additional functionalities to a class without affecting any of the clients.
  • When extending the functionalities of a class using inheritance will end up in creating lots of sub-classes.

Implementation of Decorator Design Pattern

In this example, we already have a PlainPizza class that implements Pizza interface. Now we want to add an extra functionality of adding extra cheese to pizza by keeping the interface same as Pizza.
Decorator Design Pattern UML Diagram
We will first create an Pizza interface and it's concrete implementation PlainPizza.

Pizza.java
public interface Pizza {
    public float getPrice();
    public void preparePizza();
    public void packPizza();
}
PlainPizza.java
public class PlainPizza implements Pizza {
    @Override
    public float getPrice(){
        return 10;
    }
 
    @Override 
    public void preparePizza(){
        System.out.println("Prepairing Pizza");
    }
 
    @Override
    public void packPizza(){
        System.out.println("Packing Pizza");
    }
}

Create abstract PizzaDecorator class implementing the Pizza interface and concrete decorator class extending PizzaDecorator class. This will ensure that all decorator classes will have common Pizza interface.

PizzaDecorator.java
public abstract class PizzaDecorator implements Pizza {
 
    protected Pizza pizza;
 
    public PizzaDecorator(Pizza pizza) {
        this.pizza = pizza;
    }
 
    public float getPrice(){
        return pizza.getPrice();
    }
 
    public void preparePizza(){
        pizza.preparePizza();
    }
 
    public void packPizza(){
        pizza.packPizza();
    }
}

CheeseBurstPizza.java class overrides the Pizza interface methods to provide additional functionalities or adding more cheese toppings.

CheeseBurstPizza.java
public class CheeseBurstPizza extends PizzaDecorator {
 
    public CheeseBurstPizza(Pizza pizza){
        super(pizza);
    }
 
    @Override
    public float getPrice(){
        // Extra money for Cheese Burst 
        return pizza.getPrice() + 5.0f;
    }
 
    @Override
    public void preparePizza(){
        System.out.println("Adding Extra Cheese..");
        pizza.preparePizza();
    }
 
    @Override
    public void packPizza(){
        pizza.packPizza();
    }
}

DecoratorPatternExample.java class creates a plane pizza and Cheese Burst Pizza using Pizza interface.

DecoratorPatternExample.java
public class DecoratorPatternExample {
  public static void main(String[] args) { 
      // Prepairing a Plain pizza
      Pizza plainPizza = new PlainPizza();
      plainPizza.preparePizza();
      plainPizza.packPizza();
      System.out.println(plainPizza.getPrice());
      System.out.println("---------------------------");
   
      // Prepairing a Cheese Burst Pizza using Pizza interface only
      Pizza cheeseBurstPizza = new CheeseBurstPizza(new PlainPizza());
      cheeseBurstPizza.preparePizza();
      cheeseBurstPizza.packPizza();
      System.out.println(cheeseBurstPizza.getPrice());
  }
}

Output

Prepairing Pizza
Packing Pizza
10.0
---------------------------
Adding Extra Cheese..
Prepairing Pizza
Packing Pizza
15.0

Best Practices of Decorator Design Pattern

To make the most effective use of the Decorator Design Pattern, it's essential to follow best practices that contribute to a maintainable and flexible implementation:
  • Ensure that the component and decorators remain independent. The component should not be aware of the existence of decorators, and decorators should not rely on specific implementations of the component.

  • Design the component interface and concrete components with extensibility in mind. Components should be easily extendable by decorators without modifying existing code.

  • Design a clear and concise component interface that declares the common interface for both concrete components and decorators. The interface should define the basic operations that decorators can enhance.

  • Favor composition over inheritance when applying the Decorator pattern. Composition allows for more flexible and dynamic combinations of behaviors, making the system more adaptable.

  • Avoid creating a large number of nested decorators, as this can lead to increased complexity. Instead, use decorators judiciously to add specific responsibilities and behaviors.

  • Follow the Open-Closed Principle by allowing the introduction of new decorators without modifying existing code. This principle promotes a design that is open for extension but closed for modification.

  • When creating concrete decorators, consider using interfaces that extend the component interface. This approach allows for more flexibility in combining decorators and avoids unnecessary class hierarchies.

  • Choose meaningful names for decorator classes that reflect the specific responsibility they add. Clear names enhance code readability and make it easier for developers to understand the purpose of each decorator.

Related Topics
Strategy Design Pattern
Builder Design Pattern
Bridge Design Pattern
Memento Design Pattern
Mediator Design Pattern
List of Design Patterns