Enhancing Object Behavior with the Decorator Design Pattern

In object-oriented programming, extending the behavior of an object is usually achieved through inheritance or composition. However, these techniques apply changes at compile-time, making them inflexible at runtime. This is where the Decorator Design Pattern becomes particularly useful—it enables modifications to an object’s behavior dynamically at runtime.

Imagine you need to implement different types of cars, each with specific features. Traditionally, you might create a Car interface, a basic implementation like BasicCar, and further extend it to SportsCar and LuxuryCar subclasses. The class hierarchy might look like this:

Car → BasicCar → SportsCar / LuxuryCar

But if you want to create a car that combines both sports and luxury features at runtime, the situation becomes more complex. What if you need to specify the order in which these features are added? With ten different car types, managing all combinations using inheritance alone becomes almost impossible. This is exactly where the Decorator Pattern shines.

How the Decorator Pattern Works

The Decorator Pattern allows behavior to be added to an individual object dynamically, without affecting the behavior of other objects from the same class. This is particularly useful when an object’s functionality needs to be extended at runtime without modifying the object’s structure.

To implement the Decorator Pattern, the following components are necessary:

  1. Component Interface: The core interface or abstract class that defines the contract for all objects. In this case, Car represents the component interface.

 
   public interface Car {
       public void assemble();
   }

  1. Component Implementation: A basic implementation of the component interface, such as BasicCar.

 
   public class BasicCar implements Car {
       @Override
       public void assemble() {
           System.out.print("Basic Car.");
       }
   }

  1. Decorator Class: This class implements the component interface and contains a reference to a Car object (the component). It serves as the base decorator class.

 
   public class CarDecorator implements Car {
       protected Car car;
       
       public CarDecorator(Car c) {
           this.car = c;
       }
       
       @Override
       public void assemble() {
           this.car.assemble();
       }
   }

  1. Concrete Decorators: These classes extend the base decorator class and add additional behavior. Examples include SportsCar and LuxuryCar.

 
   public class SportsCar extends CarDecorator {
       public SportsCar(Car c) {
           super(c);
       }
       
       @Override
       public void assemble() {
           super.assemble();
           System.out.print(" Adding features of Sports Car.");
       }
   }

   public class LuxuryCar extends CarDecorator {
       public LuxuryCar(Car c) {
           super(c);
       }
       
       @Override
       public void assemble() {
           super.assemble();
           System.out.print(" Adding features of Luxury Car.");
       }
   }

Class Diagram of the Decorator Pattern

The class structure for this pattern can be visualized as follows:

  • Car (Component)
    • BasicCar (Concrete Component)
    • CarDecorator (Base Decorator)
      • SportsCar (Concrete Decorator)
      • LuxuryCar (Concrete Decorator)

Testing the Decorator Pattern

Here’s how you can use this pattern in a test scenario. You can create objects of SportsCar, LuxuryCar, or combine them dynamically at runtime:

 
public class DecoratorPatternTest {
    public static void main(String[] args) {
        Car sportsCar = new SportsCar(new BasicCar());
        sportsCar.assemble();
        System.out.println("\n*****");
        
        Car sportsLuxuryCar = new SportsCar(new LuxuryCar(new BasicCar()));
        sportsLuxuryCar.assemble();
    }
}

Output:

 
Basic Car. Adding features of Sports Car.
*****
Basic Car. Adding features of Luxury Car. Adding features of Sports Car.

As you can see, the client program can create different car objects at runtime and even specify the order in which features are added.

Key Advantages of the Decorator Pattern

  • Runtime Flexibility: The biggest advantage of the Decorator Pattern is its ability to modify object behavior at runtime, offering unparalleled flexibility compared to inheritance-based designs.
  • Maintenance & Extensibility: Adding or modifying behavior becomes easier because decorators can be composed in various combinations, making the system more scalable.

Drawbacks

  • Increased Complexity: Since each feature requires its own decorator, the number of decorator classes may increase, making it harder to manage when numerous features need to be added.

Common Use Cases

In Java, the Decorator Pattern is commonly used in the I/O classes like FileReader, BufferedReader, and others, where additional functionality is added to readers and writers at runtime.

Create a Free Account

Register now and get access to our Cloud Services.

Posts you might be interested in: