Skip to Content
Course content

141: The Decorator Pattern (Structural)

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're at a coffee shop. You start with a basic black coffee. Then, you decide you want milk. Then you decide you also want sugar. Finally, you add a shot of vanilla. You aren't buying four different drinks; you're taking one core object (the coffee) and "decorating" it with additional ingredients. Each addition changes the cost and the description, but at the end of the day, it's still just a coffee.

In C++, the Decorator pattern does exactly this. It allows you to add behavior to an individual object, dynamically, without affecting the behavior of other objects from the same class. You might be wondering, "Why not just use inheritance?" Well, if you used inheritance for our coffee shop, you'd end up with a nightmare of classes: CoffeeWithMilk, CoffeeWithSugar, CoffeeWithMilkAndSugar, CoffeeWithMilkSugarAndVanilla... the combinatorial explosion would kill your productivity (and your sanity) very quickly.

Avoiding the Inheritance Explosion

The magic of the Decorator pattern is that it uses composition instead of inheritance to extend functionality. Instead of creating a new class for every possible combination, we create "wrapper" classes. Each wrapper implements the same interface as the object it's wrapping. This means the client code doesn't even know it's talking to a decorator; it just thinks it's talking to a Coffee object.

Here is how we map our coffee shop to code. We define a base interface, a concrete implementation, and then a decorator base class that holds a pointer to that interface.

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

// The Component interface
class Coffee {
public:
    virtual ~Coffee() = default;
    virtual std::string getDescription() const = 0;
    virtual double getCost() const = 0;
};

// The Concrete Component
class SimpleCoffee : public Coffee {
public:
    std::string getDescription() const override {
        return "Simple Coffee";
    }
    double getCost() const override {
        return 2.00;
    }
};

// The Decorator base class
// It MUST inherit from Coffee so it can be used anywhere a Coffee is expected
class CoffeeDecorator : public Coffee {
protected:
    std::unique_ptr wrappedCoffee;
public:
    CoffeeDecorator(std::unique_ptr coffee) : wrappedCoffee(std::move(coffee)) {}
};

// Concrete Decorator A
class MilkDecorator : public CoffeeDecorator {
public:
    using CoffeeDecorator::CoffeeDecorator; // Inherit constructor

    std::string getDescription() const override {
        return wrappedCoffee->getDescription() + ", Milk";
    }
    double getCost() const override {
        return wrappedCoffee->getCost() + 0.50;
    }
};

// Concrete Decorator B
class SugarDecorator : public CoffeeDecorator {
public:
    using CoffeeDecorator::CoffeeDecorator;

    std::string getDescription() const override {
        return wrappedCoffee->getDescription() + ", Sugar";
    }
    double getCost() const override {
        return wrappedCoffee->getCost() + 0.20;
    }
};

Layering Functionality in Practice

Notice how the MilkDecorator doesn't replace the SimpleCoffee; it wraps it. When you call getCost() on the milk decorator, it asks the inner coffee for its price and then adds its own surcharge. I've seen developers try to implement this with a complex std::vector of modifiers inside a single class, but that often leads to a "God Object" that knows too much about every possible modifier. The Decorator pattern keeps the logic for "Milk" strictly inside the MilkDecorator.

Here is how you actually assemble these objects at runtime:

int main() {
    // Start with a simple coffee
    std::unique_ptr myDrink = std::make_unique<SimpleCoffee>();
    
    // Wrap it in milk
    myDrink = std::make_unique<MilkDecorator>(std::move(myDrink));
    
    // Wrap it in sugar
    myDrink = std::make_unique<SugarDecorator>(std::move(myDrink));
    
    // Wrap it in milk AGAIN (double milk!)
    myDrink = std::make_unique<MilkDecorator>(std::move(myDrink));

    std::cout < "Order: " < myDrink->getDescription() < std::endl;
    std::cout < "Total: $" < myDrink->getCost() < std::endl;

    return 0;
}

In the example above, myDrink becomes a set of nesting dolls. The outermost MilkDecorator calls the SugarDecorator, which calls the inner MilkDecorator, which finally calls the SimpleCoffee. The result bubbles back up, accumulating the price and the description string as it goes.

When to actually use this

Don't use this for everything. If you only have two options that never change, a simple boolean flag or a small enum is fine. But the moment you find yourself creating classes like PremiumUserWithAdminRightsAndBetaAccess, stop. That's a signal that you should be using decorators. It's the perfect tool for "opt-in" features where the user can mix and match capabilities at runtime.




📋 Practical Task

Building a Modifiable RPG Weapon System

In many RPGs, a base weapon (like a Sword) can be enchanted with various effects (Fire, Ice, Lightning) that add to its attack power. Your task is to implement this using the Decorator pattern.

  • Create a Weapon base class with two virtual methods: std::string getName() and int getDamage().
  • Implement a BasicSword class that returns "Basic Sword" and a damage value of 10.
  • Create a WeaponDecorator base class that holds a std::unique_ptr<Weapon>.
  • Implement two concrete decorators:
    • FireEnchantment: Adds " (Fire)" to the name and increases damage by 5.
    • IceEnchantment: Adds " (Ice)" to the name and increases damage by 3.
  • In your main function, create a BasicSword, wrap it in a FireEnchantment, and then wrap that result in an IceEnchantment.
  • Print the final name and damage to the console to verify that the values have accumulated correctly.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.