Decorator Pattern: Enhancing Low-Level Design Flexibility
Design Pattern

Decorator Pattern: Enhancing Low-Level Design Flexibility

S

Shivam Chauhan

12 days ago

Ever felt stuck trying to add features to an existing object without messing with its core code? I’ve been there. You want to extend functionality, not rewrite the whole thing. That’s where the Decorator Pattern comes in. It's like adding layers to a cake, each layer enhancing the original without changing it. It's a structural design pattern that allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class. Let's get into it.

What is the Decorator Pattern?

The Decorator Pattern allows you to add responsibilities to objects dynamically. Think of it as wrapping an object with one or more decorators that add new functionality. This is a flexible alternative to subclassing because it lets you add responsibilities to individual objects without affecting other objects of the same class.

When to Use the Decorator Pattern

Use the Decorator Pattern when:

  • You need to add responsibilities to individual objects dynamically.
  • You want to avoid creating a large number of subclasses to extend functionality.
  • The addition of responsibilities might be withdrawn at runtime.

Implementation in Java

Let's look at a Java example. Suppose we have a simple Coffee interface and concrete implementations like SimpleCoffee. We can add decorators to enhance the coffee with milk, sugar, or chocolate.

java
// Component Interface
interface Coffee {
    String getDescription();
    double getCost();
}

// Concrete Component
class SimpleCoffee implements Coffee {
    @Override
    public String getDescription() {
        return "Simple Coffee";
    }

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

// Decorator
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee decoratedCoffee) {
        this.decoratedCoffee = decoratedCoffee;
    }

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

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

// Concrete Decorators
class Milk extends CoffeeDecorator {
    public Milk(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription() + ", with Milk";
    }

    @Override
    public double getCost() {
        return decoratedCoffee.getCost() + 0.5;
    }
}

class Sugar extends CoffeeDecorator {
    public Sugar(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription() + ", with Sugar";
    }

    @Override
    public double getCost() {
        return decoratedCoffee.getCost() + 0.2;
    }
}

// Client Code
public class DecoratorPatternExample {
    public static void main(String[] args) {
        Coffee coffee = new SimpleCoffee();
        System.out.println("Cost: " + coffee.getCost() + ", Description: " + coffee.getDescription());

        coffee = new Milk(coffee);
        System.out.println("Cost: " + coffee.getCost() + ", Description: " + coffee.getDescription());

        coffee = new Sugar(coffee);
        System.out.println("Cost: " + coffee.getCost() + ", Description: " + coffee.getDescription());
    }
}

In this example:

  • Coffee is the component interface that defines the base object.
  • SimpleCoffee is a concrete component that implements the Coffee interface.
  • CoffeeDecorator is an abstract decorator that implements the Coffee interface and holds a reference to a Coffee object.
  • Milk and Sugar are concrete decorators that add additional responsibilities to the Coffee object.

Benefits of the Decorator Pattern

  • Flexibility: Add or remove responsibilities dynamically at runtime.
  • Avoids Subclassing: Extends an object’s behavior without creating many subclasses.
  • Single Responsibility Principle: Each decorator has a specific responsibility, making the code modular and maintainable.

Drawbacks of the Decorator Pattern

  • Complexity: Can lead to a large number of classes, increasing complexity.
  • Order Matters: The order of decorators can affect the final behavior and cost.

Real-World Example: File I/O Streams

In Java’s java.io package, the Decorator Pattern is used extensively with streams. For example, you can wrap a FileInputStream with a BufferedInputStream to add buffering, and then wrap it with a DataInputStream to read primitive data types.

java
FileInputStream fileInputStream = new FileInputStream("test.txt");
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
DataInputStream dataInputStream = new DataInputStream(bufferedInputStream);

int data = dataInputStream.readInt();

Here, BufferedInputStream and DataInputStream are decorators that add functionality to the base FileInputStream.

UML Diagram (React Flow)

Here’s a React Flow UML diagram illustrating the structure of the Decorator Pattern:

Drag: Pan canvas

FAQs

Q: When should I use the Decorator Pattern instead of inheritance?

Use the Decorator Pattern when you need to add responsibilities to individual objects dynamically and avoid creating a large number of subclasses. Inheritance is static and applies to all instances of a class.

Q: Can I apply multiple decorators to an object?

Yes, you can apply multiple decorators to an object to add multiple responsibilities. The order in which you apply the decorators can affect the final behavior.

Q: Does the Decorator Pattern violate the Open/Closed Principle?

No, the Decorator Pattern adheres to the Open/Closed Principle because it allows you to add new functionality without modifying the existing code.

Conclusion

The Decorator Pattern is a valuable tool for enhancing low-level design flexibility. By adding responsibilities dynamically, you can create more maintainable and extensible code. It's all about layering functionality without altering the core. For hands-on practice with the Decorator Pattern and other design patterns, explore problems at Coudo AI, where practical exercises and AI-driven feedback can enhance your learning experience. Check out our related problems such as:

Next time you're thinking about how to extend an object's behavior, remember the Decorator Pattern—it might just be the perfect fit. \n\n

About the Author

S

Shivam Chauhan

Sharing insights about system design and coding practices.