Decorator Pattern: Add Behavior Without Subclassing

The Decorator Pattern is a structural design pattern that allows behavior to be added to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class. This pattern is particularly useful in scenarios where subclassing would lead to an explosion of classes due to the need for various combinations of behaviors.

Key Concepts

  • Component: This is the interface or abstract class that defines the methods that can be implemented by concrete components and decorators.
  • Concrete Component: This is a class that implements the Component interface. It defines the core behavior that can be enhanced by decorators.
  • Decorator: This is an abstract class that implements the Component interface and contains a reference to a Component object. It delegates the calls to the wrapped component and can add additional behavior before or after the call.
  • Concrete Decorators: These are classes that extend the Decorator class and add specific behaviors or responsibilities.

How It Works

The Decorator Pattern works by wrapping a concrete component with a decorator that adds new functionality. This allows for flexible and reusable code. Here’s a simple example in Python:

class Coffee:
    def cost(self):
        return 5

class MilkDecorator:
    def __init__(self, coffee):
        self._coffee = coffee

    def cost(self):
        return self._coffee.cost() + 1

class SugarDecorator:
    def __init__(self, coffee):
        self._coffee = coffee

    def cost(self):
        return self._coffee.cost() + 0.5

# Usage
coffee = Coffee()
print(f"Cost of coffee: ${coffee.cost()}")  # Output: Cost of coffee: $5

milk_coffee = MilkDecorator(coffee)
print(f"Cost of milk coffee: ${milk_coffee.cost()}")  # Output: Cost of milk coffee: $6

sugar_milk_coffee = SugarDecorator(milk_coffee)
print(f"Cost of sugar milk coffee: ${sugar_milk_coffee.cost()}")  # Output: Cost of sugar milk coffee: $6.5

In this example, we have a Coffee class that represents a basic coffee. The MilkDecorator and SugarDecorator classes add additional costs to the coffee without modifying the original Coffee class. This demonstrates how decorators can enhance functionality dynamically.

Advantages

  • Flexibility: You can add or remove behaviors at runtime without altering the existing code.
  • Single Responsibility Principle: Each decorator can focus on a specific behavior, adhering to the principle of single responsibility.
  • Avoids Class Explosion: Instead of creating numerous subclasses for every combination of behaviors, decorators allow for a more manageable approach.

Disadvantages

  • Complexity: The use of decorators can lead to a more complex system, making it harder to understand the flow of the program.
  • Overhead: Each decorator adds a layer of abstraction, which can introduce performance overhead.

Conclusion

The Decorator Pattern is a powerful tool in object-oriented design that allows developers to extend the functionality of objects without the need for extensive subclassing. By understanding and applying this pattern, software engineers and data scientists can create more flexible and maintainable code, which is essential for technical interviews and real-world applications.