Skip to content

Decorator Pattern

Intent

The Decorator pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

Also known as: Wrapper

Problem

Sometimes you need to add responsibilities to individual objects, not to an entire class. Using inheritance to add functionality has several limitations:

  • It's static - you can't add or remove responsibilities at runtime
  • It can lead to class explosion when you need multiple combinations
  • You can't add the same responsibility multiple times
  • Subclassing requires modifying the original class hierarchy

For example, if you have a text component and want to add borders, scrollbars, or both, creating subclasses for every combination (TextWithBorder, TextWithScrollbar, TextWithBorderAndScrollbar) quickly becomes unmanageable.

Solution

The Decorator pattern suggests wrapping an object inside a decorator object that adds new behavior. Multiple decorators can be stacked to combine behaviors. Each decorator:

  • Implements the same interface as the wrapped object
  • Adds its own behavior before/after delegating to the wrapped object
  • Can be combined with other decorators in any order

Structure

Decorator Pattern Diagram 0

Participants

  • Component - Defines the interface for objects that can have responsibilities added - In our example: Component interface with operation() method
  • ConcreteComponent - Defines an object to which additional responsibilities can be attached - The base object that will be decorated - In our example: ConcreteComponent class
  • Decorator - Maintains a reference to a Component object - Defines an interface that conforms to Component's interface - In our example: Decorator abstract class
  • ConcreteDecorator - Adds responsibilities to the component - Can add state and behavior - In our example: ConcreteDecoratorA and ConcreteDecoratorB
  • Client - Works with objects through the Component interface - Doesn't need to know about decorators

Implementation

Component Interface

class Component {
public:
    virtual ~Component() = default;
    virtual std::string operation() const = 0;
};

class ConcreteComponent : public Component {
public:
    std::string operation() const override {
        return "ConcreteComponent";
    }
};

Decorator Classes

class Decorator : public Component {
protected:
    std::unique_ptr<Component> component_;

public:
    explicit Decorator(std::unique_ptr<Component> component)
        : component_(std::move(component)) {}

    std::string operation() const override {
        return this->component_->operation();
    }
};

class ConcreteDecoratorA : public Decorator {
public:
    explicit ConcreteDecoratorA(std::unique_ptr<Component> component)
        : Decorator(std::move(component)) {}

    std::string operation() const override {
        return "ConcreteDecoratorA(" + Decorator::operation() + ")";
    }
};

class ConcreteDecoratorB : public Decorator {
public:
    explicit ConcreteDecoratorB(std::unique_ptr<Component> component)
        : Decorator(std::move(component)) {}

    std::string operation() const override {
        return "ConcreteDecoratorB(" + Decorator::operation() + ")";
    }
};

Usage Example

// Client works with components uniformly
void clientCode(const Component* component) {
    std::cout << "RESULT: " << component->operation() << std::endl;
}

int main() {
    // Simple component
    auto simple = std::make_unique<ConcreteComponent>();
    clientCode(simple.get());

    // Single decorator
    auto component1 = std::make_unique<ConcreteComponent>();
    auto decorator1 = std::make_unique<ConcreteDecoratorA>(std::move(component1));
    clientCode(decorator1.get());

    // Multiple decorators stacked
    auto component2 = std::make_unique<ConcreteComponent>();
    auto decoratorA = std::make_unique<ConcreteDecoratorA>(std::move(component2));
    auto decoratorB = std::make_unique<ConcreteDecoratorB>(std::move(decoratorA));
    clientCode(decoratorB.get());

    return 0;
}

Applicability

Use the Decorator pattern when:

  • You need to add responsibilities to objects dynamically and transparently - Without affecting other objects - Example: Adding features to a window at runtime
  • You need to add responsibilities that can be withdrawn - Decorators can be removed by unwrapping - Example: Temporarily enabling logging
  • Extension by subclassing is impractical - Too many combinations would result in class explosion - The class definition is hidden or unavailable for subclassing
  • You want to add responsibilities to individual objects, not to all objects of a class - Some instances need extra functionality, others don't - Example: Some text fields need validation, others don't

Consequences

Benefits

  1. More flexibility than static inheritance - Add or remove responsibilities at runtime - Mix and match decorators in any combination

  2. Avoids feature-laden classes high in the hierarchy - Start with simple classes - Add functionality incrementally with decorators

  3. Single Responsibility Principle - Each decorator focuses on one responsibility - Behaviors can be combined as needed

  4. Open/Closed Principle - Add new decorators without modifying existing code - Extend functionality without changing original classes

  5. Can apply the same decorator multiple times - Stack same decorator for cumulative effect - Example: Multiple layers of encryption

Drawbacks

  1. Lots of small objects - System can become complex with many decorators - Can be harder to understand and debug

  2. Decorator and component aren't identical - Can't rely on object identity - Type checking may fail if expecting specific type

  3. Ordering matters - Different stacking orders can produce different results - Must carefully consider decorator sequence

  4. Configuration complexity - Creating complex decorator combinations can be cumbersome - Consider using Factory or Builder patterns to help

Decorator vs Similar Patterns

Decorator vs Adapter

Aspect Decorator Adapter
Intent Add responsibilities without changing interface Make incompatible interfaces work together
Interface Keeps the same interface Changes the interface
Purpose Enhance functionality Interface conversion
Number Can stack multiple decorators Typically single adapter

Decorator vs Proxy

Aspect Decorator Proxy
Intent Add functionality Control access
Transparency Client knows about decoration Client unaware of proxy
Multiple Can stack many decorators Usually single proxy
Interface Can add new operations Same interface as subject

Decorator vs Composite

Aspect Decorator Composite
Intent Add responsibilities Represent part-whole hierarchies
Structure Linear chain Tree structure
Purpose Enhance single object Treat individuals and groups uniformly
Multiple children No (single wrapped object) Yes (multiple children)

Real-world Examples

  1. I/O Streams - Component: FileStream - Decorators: BufferedStream, CompressedStream, EncryptedStream - Example: EncryptedStream(CompressedStream(BufferedStream(FileStream))) - Java's InputStream hierarchy uses this pattern extensively

  2. GUI Components - Component: TextBox, Button - Decorators: ScrollDecorator, BorderDecorator, ShadowDecorator - Example: BorderDecorator(ScrollDecorator(TextBox)) - Java Swing's component borders use decorators

  3. Text Formatting - Component: PlainText - Decorators: BoldDecorator, ItalicDecorator, UnderlineDecorator - Example: BoldDecorator(ItalicDecorator(PlainText)) - Rich text editors often use this pattern

  4. Coffee Shop Orders - Component: Espresso, DarkRoast - Decorators: Milk, Sugar, Whip, Mocha - Example: Milk(Sugar(Espresso)) - Classic example from "Head First Design Patterns"

  5. Web Middleware - Component: HTTP Handler - Decorators: AuthenticationDecorator, LoggingDecorator, CompressionDecorator - Example: Express.js middleware chain - Each middleware wraps the next handler

  6. Logging Systems - Component: BasicLogger - Decorators: TimestampDecorator, LevelDecorator, FileOutputDecorator - Example: FileOutputDecorator(LevelDecorator(TimestampDecorator(BasicLogger)))

Implementation Considerations

1. Interface Conformance

Decorators must conform to the Component interface:

1
2
3
class Decorator : public Component {
    // Must implement all Component methods
};

2. Omitting the Abstract Decorator

If you only need one decorator, you can skip the abstract Decorator class:

1
2
3
4
class ConcreteDecorator : public Component {
    std::unique_ptr<Component> component_;
    // Direct implementation
};

3. Keeping Component Simple

Keep the Component interface lean: - Define only essential operations - Don't clutter with decorator-specific operations - Decorators can add new operations if needed

4. Changing the Skin vs Changing the Guts

Decorator (Changing the Skin): - Changes interface or adds responsibilities - Doesn't change core algorithm - Wraps from outside

Strategy (Changing the Guts): - Changes internal algorithm/behavior - Same interface, different implementation - Replaces from inside

5. Decorator Transparency

Decorators should be transparent to clients:

1
2
3
4
// Client doesn't need to know about decorators
void clientCode(Component* component) {
    component->operation(); // Works for both decorated and undecorated
}

6. Shared Decorators

Multiple objects can share the same decorator if: - Decorator is stateless - Decorator doesn't depend on wrapped component's state - Consider Flyweight pattern for shared decorators

Sample Output

=== Decorator Pattern Demo ===

Client: I've got a simple component:
RESULT: ConcreteComponent

Client: Now I've got a decorated component:
RESULT: ConcreteDecoratorA(ConcreteComponent)

Client: And a complex decorated component:
RESULT: ConcreteDecoratorB(ConcreteDecoratorA(ConcreteComponent))

Client: Decoration in reverse order:
RESULT: ConcreteDecoratorA(ConcreteDecoratorB(ConcreteComponent))

Client: Multiple layers of same decorator:
RESULT: ConcreteDecoratorA(ConcreteDecoratorA(ConcreteDecoratorA(ConcreteComponent)))

=== Real-world Scenario ===

Scenario: I/O Streams and Data Processing
- Component: FileStream (basic file I/O)
- Decorator: BufferedStream (adds buffering)
- Decorator: CompressedStream (adds compression)
- Decorator: EncryptedStream (adds encryption)

Examples:
1. FileStream -> Basic file operations
2. BufferedStream(FileStream) -> Buffered file I/O
3. CompressedStream(BufferedStream(FileStream)) -> Compressed + Buffered file I/O
4. EncryptedStream(CompressedStream(BufferedStream(FileStream))) -> Encrypted + Compressed + Buffered file I/O

Benefits:
- Add functionality dynamically at runtime
- Combine decorators in any order
- No need to modify original classes
- Each decorator is independent and reusable

Other real-world examples:
- GUI components: ScrollDecorator, BorderDecorator for widgets
- Text formatting: BoldDecorator, ItalicDecorator for text
- Coffee shop: MilkDecorator, SugarDecorator for beverages
- Logging: TimestampDecorator, LevelDecorator for log messages

Key Takeaways

  1. Decorator adds responsibilities to objects dynamically without modifying their class
  2. More flexible than inheritance - can add/remove decorations at runtime
  3. Decorators implement the same interface as the objects they wrap
  4. Multiple decorators can be stacked to combine behaviors
  5. Avoids class explosion from having to subclass for every combination
  6. Each decorator focuses on a single responsibility (SRP)
  7. New decorators can be added without modifying existing code (OCP)
  8. Order of decorators matters and affects the final behavior
  9. Commonly used in I/O streams, GUI components, and middleware systems
  10. Works well with Composite, Strategy, and other structural patterns