Decorator Pattern
The Decorator is a structural design pattern that allows behavior to be added to individual objects dynamically, without affecting other objects from the same class. It uses composition to wrap objects, extending functionality at runtime (e.g., adding logging or caching to methods).
Key Principles
- Composition over Inheritance: Wrap objects instead of subclassing for flexibility.
- Open/Closed Principle: Extend behavior without modifying core classes.
- Single Responsibility: Decorators focus on one enhancement.
Structure
- Component: Interface/abstract class for objects to decorate.
- Concrete Component: Basic implementation.
- Decorator: Abstract wrapper implementing Component, holding a reference to the wrapped object.
- Concrete Decorator: Adds specific behavior (e.g., logging).
Python Example: Coffee Shop (Beverage with Add-ons)
from abc import ABC, abstractmethod
# Component Interface
class Beverage(ABC):
@abstractmethod
def cost(self) -> float:
pass
@abstractmethod
def description(self) -> str:
pass
# Concrete Component
class Espresso(Beverage):
def cost(self) -> float:
return 1.99
def description(self) -> str:
return "Espresso"
class HouseBlend(Beverage):
def cost(self) -> float:
return 0.99
def description(self) -> str:
return "House Blend Coffee"
# Decorator (Abstract Wrapper)
class BeverageDecorator(Beverage):
def __init__(self, beverage: Beverage):
self.beverage = beverage
def cost(self) -> float:
return self.beverage.cost()
def description(self) -> str:
return self.beverage.description()
# Concrete Decorators (Add-ons)
class Milk(BeverageDecorator):
def cost(self) -> float:
return super().cost() + 0.10
def description(self) -> str:
return f"{super().description()}, Milk"
class Sugar(BeverageDecorator):
def cost(self) -> float:
return super().cost() + 0.05
def description(self) -> str:
return f"{super().description()}, Sugar"
class Whip(BeverageDecorator):
def cost(self) -> float:
return super().cost() + 0.15
def description(self) -> str:
return f"{super().description()}, Whip"
# Usage (Client Code)
if __name__ == "__main__":
# Basic beverage
espresso = Espresso()
print(f"{espresso.description()}: ${espresso.cost()}")
# Decorated (dynamic add-ons)
decorated = Whip(Sugar(Milk(espresso)))
print(f"{decorated.description()}: ${decorated.cost()}")
# Another variant
house_blend = HouseBlend()
with_milk = Milk(house_blend)
print(f"{with_milk.description()}: ${with_milk.cost()}")
Output:
Espresso: $1.99
Espresso, Milk, Sugar, Whip: $2.29
House Blend Coffee, Milk: $1.09
When to Use Decorator
- Runtime Flexibility: Add/remove features dynamically (e.g., logging, validation).
- Avoid Subclass Explosion: Many combinations without deep inheritance.
- Avoid: If behavior is static (use inheritance).
Summary: Decorator wraps objects to add behavior incrementally via composition, enabling flexible extensions without altering the original class.