Python Decorator Design Pattern

1. Definition

The Decorator Design Pattern allows you to attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

2. Problem Statement

Consider a situation where you have a simple coffee and you want to add different add-ons like milk, whipped cream, caramel, etc. If you subclass each variant, the number of subclasses can grow exponentially, making it impractical.

3. Solution

Instead of using subclassing, you "wrap" the original object with decorator objects which add the new functionalities. Each decorator wraps the original object, thereby building the features step by step.

4. Real-World Use Cases

1. GUI libraries where you want to add behaviors like scrolling, bordering dynamically to windows.

2. Adding logging, authentication, and other cross-cutting concerns in web applications.

3. Enhancing the behavior of classes in financial systems without modifying the original code.

5. Implementation Steps

1. Define a base component interface.

2. Create a concrete component that implements the base component.

3. Create a base decorator class that also implements the base component.

4. Create concrete decorator classes that add additional responsibilities.

6. Implementation in Python

# Step 1: Define a base component
class Coffee:
    def cost(self):
        return 5
# Step 2: Concrete component
class SimpleCoffee(Coffee):
    def cost(self):
        return 5
# Step 3: Base decorator
class CoffeeDecorator(Coffee):
    def __init__(self, coffee):
        self._coffee = coffee
    def cost(self):
        return self._coffee.cost()
# Step 4: Concrete decorators
class MilkDecorator(CoffeeDecorator):
    def cost(self):
        return super().cost() + 2
class CaramelDecorator(CoffeeDecorator):
    def cost(self):
        return super().cost() + 3
# Client code
coffee = SimpleCoffee()
print(f"Cost of simple coffee: {coffee.cost()}")
coffee_with_milk = MilkDecorator(coffee)
print(f"Cost of coffee with milk: {coffee_with_milk.cost()}")
coffee_with_milk_caramel = CaramelDecorator(coffee_with_milk)
print(f"Cost of coffee with milk and caramel: {coffee_with_milk_caramel.cost()}")

Output:

Cost of simple coffee: 5
Cost of coffee with milk: 7
Cost of coffee with milk and caramel: 10

Explanation:

1. Coffee is the component interface with a method cost().

2. SimpleCoffee is the concrete component implementing the Coffee interface.

3. CoffeeDecorator is a base decorator that also implements the Coffee interface and has a reference to a Coffee object.

4. MilkDecorator and CaramelDecorator are concrete decorators that add additional cost to the coffee.

5. The client code creates a simple coffee and then decorates it with milk and caramel, calculating the total cost.

7. When to use?

Use the Decorator pattern when:

1. You need to add responsibilities to objects dynamically and transparently without affecting other objects.

2. Extending responsibilities using subclassing is impractical due to the sheer number of subclasses that could be created.

3. You want to keep new responsibilities in separate classes (Single Responsibility Principle).


Comments