Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 66

Dynamic Object Enhancement with Decorator

Recommended resources

Hello! Welcome to our lesson on the Decorator pattern.

In our last session, we dissected the Adapter pattern, learning how it acts as a translator to make incompatible interfaces work together. We established its core intent: interface conversion.

Today, we'll explore another structural pattern that also uses object wrapping but for a completely different purpose: the Decorator pattern. While an Adapter changes an object's interface to match what a client expects, a Decorator enriches an object with new responsibilities while keeping the interface consistent. This allows you to add features to objects dynamically and transparently.

Our learning outcome is to implement the Decorator pattern to dynamically add responsibilities to objects without affecting other objects. We will see how this pattern provides a flexible alternative to subclassing for extending functionality.

1. The Problem: The Limits of Static Inheritance

Imagine you're building a graphics application. You start with a base Shape interface and two concrete classes, Circle and Square.

// Component Interface
struct Shape {
public:
    virtual std::string str() const = 0;
};

// Concrete Components
struct Circle : public Shape { /* ... */ };
struct Square : public Shape { /* ... */ };

Now, a new requirement comes in: shapes need to have color. A natural first thought, using inheritance, is to create subclasses like ColoredCircle and ColoredSquare.

struct ColoredCircle : public Circle {
    std::string color;
    // ...
};

struct ColoredSquare : public Square {
    std::string color;
    // ...
};

This seems manageable. But what happens when you also need to add borders? Or textures? Or transparency? You'd need BorderedCircle, TexturedCircle, TransparentCircle, ColoredBorderedCircle, ColoredTexturedCircle, and so on.

This rapid proliferation of subclasses to handle every possible combination of features is known as class explosion. It's rigid, unmanageable, and violates the Single Responsibility Principle, as a class like ColoredBorderedCircle is responsible for defining a circle, a color, and a border. This is where the Decorator pattern offers a more elegant solution.

Understanding the Decorator Pattern in C++

To see this problem laid out with code, let's turn to the article 'Understanding the Decorator Pattern in C++' by Amresh Kumar. It provides a great illustration of class explosion before introducing the pattern.

Please read the sections 'What is Decorator Pattern?' and 'Life Without the Decorator Pattern'. Focus on how the inheritance-based approach leads to the 'class explosion' and its limitations.


2. The Decorator Solution: Composition Over Inheritance

The Decorator pattern solves this problem by embracing the principle of composition over inheritance. Instead of creating new subclasses for each feature, you create "decorator" classes that wrap the original object.

The key idea is that both the original object (the "component") and the decorators share the same interface. A decorator receives a component object in its constructor, and when a method is called on the decorator, it adds its own functionality and then delegates the call to the wrapped component.

This creates a stackable, layer-like structure. You can wrap a Circle object in a ColorDecorator, and then wrap that result in a BorderDecorator. The client code still just sees a Shape object and interacts with it normally, oblivious to the layers of decoration.

The pattern has four main participants:

  1. Component: The abstract interface for objects that can have responsibilities added to them. (Shape)
  2. Concrete Component: The core object we want to decorate. (Circle, Square)
  3. Decorator: An abstract class that also conforms to the Component interface and holds a reference (typically a pointer or smart pointer) to a Component object.
  4. Concrete Decorator: The class that adds a specific responsibility. (ColorDecorator, BorderDecorator)

Let's look at a standard visualization of this structure.

UML Diagram for Decorator Pattern (Cake Example)
This UML diagram illustrates the Decorator pattern using a `Cake` analogy. `Cake` is the component interface. `PlainCake` is the concrete component. `CakeDecorator` is the abstract decorator, which both implements the `Cake` interface and holds a reference to a `Cake` object. `ChocolateDecorator` and `CaramelDecorator` are concrete decorators that add new behavior before or after delegating to the wrapped cake.

3. The Dynamic Decorator: Classic Implementation

The most common implementation of the Decorator pattern is the "dynamic" version, where objects are wrapped at runtime. This provides maximum flexibility.

Let's see how to refactor our Shape example to use decorators. We'll create a ColoredShape decorator and a BorderedShape decorator.

Understanding the Decorator Pattern in C++

The same article we started with provides a clear, step-by-step implementation of the dynamic decorator. This will show you exactly how to build the wrapping mechanism.

Read the section 'Dynamic Decorator Pattern'. Pay close attention to how ColoredShape and BorderedShape both inherit from Shape and contain a Shape& member. Notice how decorators are stacked in the main function.

Here's the essence of the implementation from the article:

// Decorator base class (optional but good practice)
// In the article's simple example, they inherit from Shape directly.
// A dedicated Decorator base class would look like this:
/*
struct ShapeDecorator : public Shape {
protected:
    Shape& decoratedShape;
public:
    ShapeDecorator(Shape& shape) : decoratedShape(shape) {}
};
*/

// Concrete Decorator for Color
struct ColoredShape : public Shape {
    std::string color;
    Shape& shape; // Composition: "has-a" Shape

    ColoredShape(const std::string& color, Shape& shape) : color(color), shape(shape) {}

    std::string str() const override {
        // Add new behavior and delegate
        return shape.str() + " having color " + color;
    }
};

// Concrete Decorator for Border
struct BorderedShape : public Shape {
    int borderWidth;
    Shape& shape; // Composition

    BorderedShape(int width, Shape& shape) : borderWidth(width), shape(shape) {}

    std::string str() const override {
        // Add new behavior and delegate
        return shape.str() + " having border " + std::to_string(borderWidth) + "px";
    }
};

int main() {
    Circle circle(10);
    Square square(5);

    // Decorate the circle with color
    ColoredShape blueCircle("blue", circle);
    std::cout << blueCircle.str() << std::endl; // "A Circle of radius 10 having color blue"

    // Decorate the square with color, then with a border
    ColoredShape redSquare("red", square);
    BorderedShape borderedRedSquare(2, redSquare);
    std::cout << borderedRedSquare.str() << std::endl; // "A Square with side 5 having color red having border 2px"
}

A Note on Memory Management

The example above uses references (Shape&), which works but implies that the decorators do not own the objects they wrap. In real-world C++, you would typically use smart pointers to manage the lifetime of the decorated objects, especially when creating them dynamically.

Using std::unique_ptr or std::shared_ptr is the modern, safe approach. For example, the decorator might take ownership of the component via std::unique_ptr.

// Decorator using a smart pointer
struct ColoredShape : public Shape {
    std::string color;
    std::unique_ptr<Shape> shape; // Manages ownership

    ColoredShape(const std::string& color, std::unique_ptr<Shape> shape)
        : color(color), shape(std::move(shape)) {}

    std::string str() const override {
        return shape->str() + " having color " + color;
    }
};

// Usage
auto circle = std::make_unique<Circle>(10);
auto blueCircle = std::make_unique<ColoredShape>("blue", std::move(circle));

This is a crucial best practice to avoid memory leaks, a topic you are familiar with from your C and systems background.

The Decorator Pattern: From Basic to Advanced Concepts ...

For more on best practices, the article 'The Decorator Pattern: From Basic to Advanced Concepts in C++' by John Farrier discusses common pitfalls, including memory management, and explicitly recommends using smart pointers.

Read the sections 'Polymorphism vs. the Decorator Pattern' and 'Intermediate Insights...'. Focus on the 'Compare and Contrast' table and the 'Common Pitfalls and Best Practices' subsection, which mentions smart pointers.


4. Advanced Decorators in C++

Given your goal of understanding C++ from first principles, it's worth knowing that the "dynamic" decorator isn't the only way. C++'s powerful features enable other implementations with different trade-offs.

Static Decorators (Compile-Time)

You can use templates to apply decorations at compile-time. This approach eliminates the runtime overhead of virtual function calls and can be more performant, at the cost of runtime flexibility.

A static decorator typically inherits from its template parameter.

template<typename T>
struct ColoredShape2 : T {
    std::string color;

    // Use variadic templates and perfect forwarding to pass args to the base constructor
    template<typename... Args>
    ColoredShape2(std::string color, Args&&... args)
        : T(std::forward<Args>(args)...), color(color) {}

    std::string str() const {
        return T::str() + " has the color " + this->color;
    }
};

Usage:

// The type itself encodes the decoration
ColoredShape2<Circle> red_circle("red", 5.0f);
// red_circle is a single object, not a chain of wrappers.
std::cout << red_circle.str() << std::endl;

This approach is extremely powerful. Because the compiler generates a specific type (ColoredShape2<Circle>), it can inline calls and perform optimizations that are impossible with the dynamic, pointer-based approach. This directly addresses your interest in how C++ maps to efficient machine code.

Functional Decorators

A third variation uses std::function to wrap callable objects (like functions or lambdas) instead of class objects. This is useful for adding cross-cutting concerns like logging or timing to a piece of logic.

Understanding the Decorator Pattern in C++

The Medium article also covers these advanced C++-specific variations. This will give you a complete picture of the different ways the decorator concept can be applied.

Read the sections 'Static Decorators' and 'Functional Decorator'. Pay attention to the use of templates, C++20 concepts, variadic templates, and std::function to see how modern C++ enables these powerful alternatives.

Test your understanding!

You are designing a notification system. You have a base Notifier interface with a send(std::string message) method. The concrete component is EmailNotifier.

You need to add functionality to send notifications via SMS and Slack as well. A single notification might need to be sent via email, SMS, and Slack simultaneously.

How would you design SMSDecorator and SlackDecorator using the dynamic Decorator pattern? Sketch out the class declarations and the send method for SMSDecorator. How would you create an object that sends a message via all three channels?

Show answer

This is a classic use case for the Decorator pattern. Each decorator adds a new notification channel without altering the core Notifier interface.

Here is the design sketch:

#include <iostream>
#include <string>
#include <memory>

// 1. Component Interface
class Notifier {
public:
    virtual ~Notifier() = default;
    virtual void send(const std::string& message) = 0;
};

// 2. Concrete Component
class EmailNotifier : public Notifier {
public:
    void send(const std::string& message) override {
        std::cout << "Sending email: " << message << std::endl;
    }
};

// 3. Abstract Decorator (Good Practice)
class NotifierDecorator : public Notifier {
protected:
    std::unique_ptr<Notifier> wrapped_;
public:
    NotifierDecorator(std::unique_ptr<Notifier> notifier) : wrapped_(std::move(notifier)) {}

    void send(const std::string& message) override {
        if (wrapped_) {
            wrapped_->send(message);
        }
    }
};

// 4. Concrete Decorators
class SMSDecorator : public NotifierDecorator {
public:
    SMSDecorator(std::unique_ptr<Notifier> notifier) : NotifierDecorator(std::move(notifier)) {}

    void send(const std::string& message) override {
        // First, call the wrapped object's send
        NotifierDecorator::send(message);
        // Then, add our own functionality
        std::cout << "Sending SMS: " << message << std::endl;
    }
};

class SlackDecorator : public NotifierDecorator {
public:
    SlackDecorator(std::unique_ptr<Notifier> notifier) : NotifierDecorator(std::move(notifier)) {}

    void send(const std::string& message) override {
        NotifierDecorator::send(message);
        std::cout << "Sending Slack message: " << message << std::endl;
    }
};

// To create an object that sends to all three channels:
int main() {
    auto notifier = std::make_unique<EmailNotifier>();
    notifier = std::make_unique<SMSDecorator>(std::move(notifier));
    notifier = std::make_unique<SlackDecorator>(std::move(notifier));

    std::cout << "Sending 'System Alert!' via all channels:\n";
    notifier->send("System Alert!");
}

The output would be:

Sending 'System Alert!' via all channels:
Sending email: System Alert!
Sending SMS: System Alert!
Sending Slack message: System Alert!

The notifier object is built by progressively wrapping the base EmailNotifier with the decorators. When send() is called on the outermost SlackDecorator, it calls the send() of its wrapped SMSDecorator, which in turn calls the send() of its wrapped EmailNotifier. After the inner calls return, each decorator adds its own behavior.


Conclusion

Today we explored the Decorator pattern, a powerful structural tool for adding functionality to objects dynamically. It provides a flexible and scalable alternative to static inheritance, adhering to the Open/Closed Principle.

Key Takeaways:

  • The Decorator pattern attaches additional responsibilities to an object dynamically by wrapping it.
  • It solves the "class explosion" problem that arises from using inheritance for all feature combinations.
  • The Dynamic Decorator is the classic runtime implementation, typically using composition with pointers (or smart pointers) to a shared interface.
  • Modern C++ enables advanced variations like Static Decorators (for compile-time efficiency) and Functional Decorators (for wrapping callables).
  • The core intent of Decorator is to add behavior while keeping the interface the same, distinguishing it from the Adapter pattern, which converts the interface.

In our next lesson, we will examine the Strategy pattern. This will offer another interesting comparison. While a Decorator changes an object's "skin" by adding layers, a Strategy changes its "guts" by swapping out core algorithms. Understanding this distinction will further deepen your command of object-oriented design.