C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
141: The Decorator Pattern (Structural)
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
Weaponbase class with two virtual methods:std::string getName()andint getDamage(). - Implement a
BasicSwordclass that returns "Basic Sword" and a damage value of 10. - Create a
WeaponDecoratorbase class that holds astd::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
mainfunction, create aBasicSword, wrap it in aFireEnchantment, and then wrap that result in anIceEnchantment. - Print the final name and damage to the console to verify that the values have accumulated correctly.
There are no comments for now.