Decorator

The Decorator Pattern is a structural design pattern that allows you to dynamically add new functionality to objects without altering their structure. It provides a flexible alternative to subclassing for extending functionality by wrapping objects in a series of decorator classes.

Core Concept

The pattern works by creating a set of decorator classes that are used to wrap concrete components. Each decorator class implements the same interface as the component it decorates, allowing decorators to be stacked and combined in various ways.

┌─────────────────┐    ┌─────────────────┐
│    Component    │    │   Decorator     │
│   (Interface)   │◄───│  (Abstract)     │
└─────────────────┘    └─────────────────┘
         ▲                       ▲
         │                       │
┌─────────────────┐    ┌─────────────────┐
│ConcreteComponent│    │ConcreteDecorator│
└─────────────────┘    └─────────────────┘

When to Use

The Decorator Pattern is useful when you need to:

  • Add responsibilities to objects dynamically and transparently
  • Avoid creating an explosion of subclasses for every combination of features
  • Compose behaviors by combining multiple decorators
  • Add or remove functionality at runtime

Key Components

The pattern consists of four main components:

Component: Defines the interface for objects that can have responsibilities added to them dynamically.

ConcreteComponent: The original object to which additional responsibilities can be attached.

Decorator: An abstract class that implements the component interface and maintains a reference to a component object.

ConcreteDecorator: Extends the decorator class to add specific responsibilities to the component.

Benefits

The Decorator Pattern offers several advantages:

  • Flexibility: You can add or remove responsibilities from an object at runtime
  • Composition over Inheritance: Avoids the need for numerous subclasses
  • Single Responsibility: Each decorator has a single, well-defined purpose
  • Open/Closed Principle: Classes are open for extension but closed for modification

Drawbacks

However, the pattern also has some limitations:

  • Complexity: Can result in many small objects that are hard to debug
  • Identity Issues: Decorated objects are not identical to the original component
  • Order Dependency: The order of decorators can affect the final behavior

Implementation Considerations

When implementing the Decorator Pattern, consider:

  • Keeping the component interface lightweight
  • Ensuring decorators can be applied in any order when possible
  • Providing a way to access the original component if needed
  • Using composition consistently throughout the decorator hierarchy

The Decorator Pattern is particularly powerful in scenarios where you need to combine multiple behaviors or features in different ways, such as adding various formatting options to text, implementing middleware in web frameworks, or creating configurable data processing pipelines.

Coffee Shop Example

This example demonstrates the Decorator Pattern using a coffee shop scenario where customers can customize their beverages with various add-ons.

from abc import ABC, abstractmethod
from typing import Protocol

class Beverage(Protocol):
    """
    Component interface that defines the contract for all beverages.
    This protocol ensures that all beverages have a description and cost.
    """
    
    def get_description(self) -> str:
        """Returns a description of the beverage."""
        ...
    
    def get_cost(self) -> float:
        """Returns the cost of the beverage."""
        ...

class Coffee(Beverage):
    """
    ConcreteComponent: A basic coffee implementation.
    This is the core object that decorators will enhance.
    """
    
    def __init__(self) -> None:
        self._description = "Simple Coffee"
        self._cost = 2.00
    
    def get_description(self) -> str:
        return self._description
    
    def get_cost(self) -> float:
        return self._cost

class Tea(Beverage):
    """
    ConcreteComponent: A basic tea implementation.
    Another core object that can be decorated.
    """
    
    def __init__(self) -> None:
        self._description = "Green Tea"
        self._cost = 1.50
    
    def get_description(self) -> str:
        return self._description
    
    def get_cost(self) -> float:
        return self._cost

class BeverageDecorator(ABC):
    """
    Abstract Decorator: Base class for all beverage decorators.
    Maintains a reference to the beverage being decorated.
    """
    
    def __init__(self, beverage: Beverage) -> None:
        self._beverage = beverage
    
    @abstractmethod
    def get_description(self) -> str:
        """Decorators must implement their own description logic."""
        pass
    
    @abstractmethod
    def get_cost(self) -> float:
        """Decorators must implement their own cost calculation."""
        pass

class MilkDecorator(BeverageDecorator):
    """
    ConcreteDecorator: Adds milk to any beverage.
    Enhances the base beverage with milk functionality.
    """
    
    def get_description(self) -> str:
        return f"{self._beverage.get_description()} + Milk"
    
    def get_cost(self) -> float:
        return self._beverage.get_cost() + 0.50

class SugarDecorator(BeverageDecorator):
    """
    ConcreteDecorator: Adds sugar to any beverage.
    Enhances the base beverage with sugar functionality.
    """
    
    def get_description(self) -> str:
        return f"{self._beverage.get_description()} + Sugar"
    
    def get_cost(self) -> float:
        return self._beverage.get_cost() + 0.25

class WhippedCreamDecorator(BeverageDecorator):
    """
    ConcreteDecorator: Adds whipped cream to any beverage.
    Enhances the base beverage with whipped cream functionality.
    """
    
    def get_description(self) -> str:
        return f"{self._beverage.get_description()} + Whipped Cream"
    
    def get_cost(self) -> float:
        return self._beverage.get_cost() + 0.75

class VanillaDecorator(BeverageDecorator):
    """
    ConcreteDecorator: Adds vanilla syrup to any beverage.
    Enhances the base beverage with vanilla flavor.
    """
    
    def get_description(self) -> str:
        return f"{self._beverage.get_description()} + Vanilla Syrup"
    
    def get_cost(self) -> float:
        return self._beverage.get_cost() + 0.60


def demonstrate_decorator_pattern() -> None:
    """
    Demonstrates the flexibility of the Decorator Pattern.
    Shows how decorators can be combined in different ways.
    """
    
    print("=== Coffee Shop Decorator Pattern Demo ===\n")
    
    # Basic beverages
    print("1. Basic Beverages:")
    coffee = Coffee()
    tea = Tea()
    
    print(f"   {coffee.get_description()}: ${coffee.get_cost():.2f}")
    print(f"   {tea.get_description()}: ${tea.get_cost():.2f}")
    print()
    
    # Single decorators
    print("2. Single Add-ons:")
    coffee_with_milk = MilkDecorator(Coffee())
    tea_with_sugar = SugarDecorator(Tea())
    
    print(f"   {coffee_with_milk.get_description()}: ${coffee_with_milk.get_cost():.2f}")
    print(f"   {tea_with_sugar.get_description()}: ${tea_with_sugar.get_cost():.2f}")
    print()
    
    # Multiple decorators (stacking)
    print("3. Multiple Add-ons (Stacked Decorators):")
    
    # Fancy coffee: Coffee + Milk + Sugar + Whipped Cream
    fancy_coffee = WhippedCreamDecorator(
        SugarDecorator(
            MilkDecorator(Coffee())
        )
    )
    
    # Vanilla tea: Tea + Vanilla + Milk
    vanilla_tea = MilkDecorator(
        VanillaDecorator(Tea())
    )
    
    print(f"   {fancy_coffee.get_description()}: ${fancy_coffee.get_cost():.2f}")
    print(f"   {vanilla_tea.get_description()}: ${vanilla_tea.get_cost():.2f}")
    print()
    
    # Demonstrating different combinations
    print("4. Same Base, Different Combinations:")
    base_coffee = Coffee()
    
    combo1 = VanillaDecorator(MilkDecorator(base_coffee))
    combo2 = WhippedCreamDecorator(SugarDecorator(Coffee()))
    combo3 = MilkDecorator(SugarDecorator(VanillaDecorator(Coffee())))
    
    print(f"   Combo 1: {combo1.get_description()}: ${combo1.get_cost():.2f}")
    print(f"   Combo 2: {combo2.get_description()}: ${combo2.get_cost():.2f}")
    print(f"   Combo 3: {combo3.get_description()}: ${combo3.get_cost():.2f}")
    print()
    
    # Show the decorator chain visualization
    print("5. Decorator Chain Visualization:")
    print("   Coffee -> +Milk -> +Sugar -> +Whipped Cream")
    print("   $2.00  -> $2.50  -> $2.75  -> $3.50")

if __name__ == "__main__":
    demonstrate_decorator_pattern()

Key Components in the Example:

Beverage Protocol: Serves as the component interface, defining the contract that all beverages must follow with get_description() and get_cost() methods.

Coffee and Tea Classes: These are concrete components representing the base beverages that can be decorated.

BeverageDecorator: An abstract base class that all decorators inherit from. It maintains a reference to the beverage being decorated.

Concrete Decorators: MilkDecorator, SugarDecorator, WhippedCreamDecorator, and VanillaDecorator. Each adds specific functionality to the base beverage.

How It Works:

The pattern allows you to wrap a beverage object with multiple decorator objects. Each decorator adds its own cost and description while delegating the base functionality to the wrapped object. This creates a chain of responsibility where each decorator enhances the beverage’s properties.

For example, when you create WhippedCreamDecorator(SugarDecorator(MilkDecorator(Coffee()))), you get:

  • Base coffee ($2.00)
  • Plus milk ($0.50)
  • Plus sugar ($0.25)
  • Plus whipped cream ($0.75)
  • Total: $3.50

The beauty of this pattern is that you can combine decorators in any order and quantity, giving you maximum flexibility without creating separate classes for every possible combination.

Text Formatter Example

This second example demonstrates the Decorator Pattern applied to text formatting, showing how multiple formatting options can be combined dynamically.

from abc import ABC, abstractmethod
from typing import Protocol

class TextComponent(Protocol):
    """
    Component interface for text formatting.
    Defines the contract for all text objects that can be formatted.
    """
    
    def render(self) -> str:
        """Returns the rendered text."""
        ...

class PlainText(TextComponent):
    """
    ConcreteComponent: Basic text implementation.
    Represents simple, unformatted text content.
    """
    
    def __init__(self, content: str) -> None:
        self._content = content
    
    def render(self) -> str:
        return self._content

class TextDecorator(ABC):
    """
    Abstract Decorator for text formatting.
    Base class for all text formatting decorators.
    """
    
    def __init__(self, text_component: TextComponent) -> None:
        self._text_component = text_component
    
    @abstractmethod
    def render(self) -> str:
        """Subclasses must implement their formatting logic."""
        pass

class BoldDecorator(TextDecorator):
    """
    ConcreteDecorator: Makes text bold.
    Wraps text with bold formatting markers.
    """
    
    def render(self) -> str:
        return f"**{self._text_component.render()}**"

class ItalicDecorator(TextDecorator):
    """
    ConcreteDecorator: Makes text italic.
    Wraps text with italic formatting markers.
    """
    
    def render(self) -> str:
        return f"*{self._text_component.render()}*"

class UnderlineDecorator(TextDecorator):
    """
    ConcreteDecorator: Underlines text.
    Wraps text with underline formatting markers.
    """
    
    def render(self) -> str:
        return f"_{self._text_component.render()}_"

class UpperCaseDecorator(TextDecorator):
    """
    ConcreteDecorator: Converts text to uppercase.
    Transforms the text content to all uppercase letters.
    """
    
    def render(self) -> str:
        return self._text_component.render().upper()

class BorderDecorator(TextDecorator):
    """
    ConcreteDecorator: Adds a border around text.
    Creates a simple ASCII border around the text.
    """
    
    def render(self) -> str:
        content = self._text_component.render()
        border_length = len(content) + 4
        border = "+" + "-" * (border_length - 2) + "+"
        
        return f"{border}\n| {content} |\n{border}"

class PrefixDecorator(TextDecorator):
    """
    ConcreteDecorator: Adds a prefix to text.
    Allows adding custom prefixes like labels or markers.
    """
    
    def __init__(self, text_component: TextComponent, prefix: str) -> None:
        super().__init__(text_component)
        self._prefix = prefix
    
    def render(self) -> str:
        return f"{self._prefix}{self._text_component.render()}"

class ColorDecorator(TextDecorator):
    """
    ConcreteDecorator: Adds color to text using ANSI codes.
    Provides basic color formatting for terminal output.
    """
    
    # ANSI color codes
    COLORS = {
        'red': '\033[91m',
        'green': '\033[92m',
        'blue': '\033[94m',
        'yellow': '\033[93m',
        'purple': '\033[95m',
        'cyan': '\033[96m',
        'reset': '\033[0m'
    }
    
    def __init__(self, text_component: TextComponent, color: str) -> None:
        super().__init__(text_component)
        self._color = color.lower()
    
    def render(self) -> str:
        color_code = self.COLORS.get(self._color, '')
        reset_code = self.COLORS['reset']
        content = self._text_component.render()
        
        if color_code:
            return f"{color_code}{content}{reset_code}"
        return content

def demonstrate_text_formatting() -> None:
    """
    Demonstrates various text formatting combinations using decorators.
    Shows how multiple formatting options can be applied flexibly.
    """
    
    print("=== Text Formatter Decorator Pattern Demo ===\n")
    
    # Basic text
    print("1. Plain Text:")
    plain = PlainText("Hello, World!")
    print(f"   '{plain.render()}'")
    print()
    
    # Single formatting
    print("2. Single Formatting:")
    bold_text = BoldDecorator(PlainText("Bold Text"))
    italic_text = ItalicDecorator(PlainText("Italic Text"))
    underline_text = UnderlineDecorator(PlainText("Underlined Text"))
    
    print(f"   {bold_text.render()}")
    print(f"   {italic_text.render()}")
    print(f"   {underline_text.render()}")
    print()
    
    # Combined formatting
    print("3. Combined Formatting:")
    
    # Bold + Italic
    bold_italic = ItalicDecorator(BoldDecorator(PlainText("Bold and Italic")))
    print(f"   {bold_italic.render()}")
    
    # All text formatting combined
    formatted_text = UnderlineDecorator(
        ItalicDecorator(
            BoldDecorator(PlainText("All Formats Combined"))
        )
    )
    print(f"   {formatted_text.render()}")
    print()
    
    # Advanced decorators
    print("4. Advanced Formatting:")
    
    # Uppercase with border
    upper_bordered = BorderDecorator(
        UpperCaseDecorator(PlainText("Important Message"))
    )
    print(upper_bordered.render())
    print()
    
    # Prefix decorator
    labeled_text = PrefixDecorator(
        BoldDecorator(PlainText("This is a warning!")),
        "WARNING: "
    )
    print(f"   {labeled_text.render()}")
    print()
    
    # Complex combination
    print("5. Complex Combination:")
    complex_text = BorderDecorator(
        UnderlineDecorator(
            BoldDecorator(
                UpperCaseDecorator(
                    PrefixDecorator(PlainText("final message"), ">>> ")
                )
            )
        )
    )
    print(complex_text.render())
    print()
    
    # Show decorator chain
    print("6. Decorator Chain Visualization:")
    print("   PlainText -> PrefixDecorator -> UpperCaseDecorator -> BoldDecorator -> UnderlineDecorator -> BorderDecorator")
    print("   'message' -> '>>> message'   -> '>>> MESSAGE'      -> '**>>> MESSAGE**' -> '_**>>> MESSAGE**_' -> [bordered]")
    print()
    
    # Different texts with same decorators
    print("7. Same Decorators, Different Content:")
    fancy_formatter = lambda text: BorderDecorator(
        BoldDecorator(
            UpperCaseDecorator(PlainText(text))
        )
    )
    
    print(fancy_formatter("Success").render())
    print()
    print(fancy_formatter("Error").render())
    print()
    print(fancy_formatter("Info").render())

def create_custom_formatter() -> TextComponent:
    """
    Factory function that creates a custom text formatter.
    Demonstrates how decorators can be composed programmatically.
    """
    def formatter(text: str, bold: bool = False, italic: bool = False, 
                 uppercase: bool = False, border: bool = False, 
                 prefix: str = "", color: str = "") -> TextComponent:
        
        component: TextComponent = PlainText(text)
        
        # Apply prefix if provided
        if prefix:
            component = PrefixDecorator(component, prefix)
        
        # Apply text transformations
        if uppercase:
            component = UpperCaseDecorator(component)
        
        # Apply formatting
        if bold:
            component = BoldDecorator(component)
        
        if italic:
            component = ItalicDecorator(component)
        
        # Apply color if provided
        if color:
            component = ColorDecorator(component, color)
        
        # Apply border last for visual clarity
        if border:
            component = BorderDecorator(component)
        
        return component
    
    return formatter


if __name__ == "__main__":
    demonstrate_text_formatting()
    
    print("\n=== Custom Formatter Demo ===")
    formatter = create_custom_formatter()
    
    # Create various formatted texts using the factory
    success_msg = formatter("Operation completed successfully!", 
                           bold=True, color="green", border=True)
                           
    error_msg = formatter("Error occurred during processing", 
                         bold=True, uppercase=True, color="red", 
                         prefix="ERROR: ")
                         
    info_msg = formatter("additional information available", 
                        italic=True, color="blue", prefix="INFO: ")
    
    print(success_msg.render())
    print()
    print(error_msg.render())
    print()
    print(info_msg.render())

Key Features of This Implementation:

  • TextComponent Protocol: Defines the interface for all text objects that can be rendered.
  • PlainText Class: The concrete component that holds the basic text content without any formatting.
  • TextDecorator Abstract Class: The base decorator that all formatting decorators inherit from.
  • Multiple Concrete Decorators: Each decorator handles a specific formatting concern:
  • BoldDecorator: Adds bold formatting
  • ItalicDecorator: Adds italic formatting
  • UnderlineDecorator: Adds underline formatting
  • UpperCaseDecorator: Transforms text to uppercase
  • BorderDecorator: Adds ASCII borders
  • PrefixDecorator: Adds custom prefixes
  • ColorDecorator: Adds ANSI color codes

Advanced Features:

The example includes a factory function create_custom_formatter() that demonstrates how decorators can be applied conditionally based on parameters. This shows how the pattern can be used to create configurable formatting systems.

Benefits Demonstrated:

  1. Flexibility: You can apply any combination of formatting in any order
  2. Reusability: The same decorators work with any text content
  3. Composability: Complex formatting is achieved by combining simple decorators
  4. Runtime Configuration: Formatting can be determined at runtime based on conditions

Pattern Benefits in Action:

Instead of creating separate classes for every formatting combination (BoldItalicText, BoldUnderlineText, etc.), the Decorator Pattern allows you to compose the exact formatting you need from individual decorators. This approach scales much better as you add more formatting options.

The text formatting example particularly highlights how decorators can transform content (like uppercase conversion) as well as wrap content (like bold formatting), showing the pattern’s versatility in handling different types of enhancements.

Track your progress

Mark this subtopic as completed when you finish reading.