Decorator Pattern
Part of the Object Oriented Programming section of Coddy's C++ journey — lesson 98 of 104.
The Decorator pattern allows you to add new behaviors to objects dynamically by wrapping them in special objects called decorators. Unlike inheritance, which adds behavior at compile time, decorators let you extend functionality at runtime without modifying the original class.
The pattern works by having both the original object and decorators implement the same interface. Each decorator holds a reference to a component and adds its own behavior before or after delegating to the wrapped object:
#include <iostream>
#include <memory>
// Component interface
class Coffee {
public:
virtual std::string getDescription() const = 0;
virtual double getCost() const = 0;
virtual ~Coffee() = default;
};
// Concrete component
class SimpleCoffee : public Coffee {
public:
std::string getDescription() const override { return "Coffee"; }
double getCost() const override { return 2.0; }
};
// Base decorator
class CoffeeDecorator : public Coffee {
protected:
std::unique_ptr<Coffee> coffee;
public:
CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};
// Concrete decorators
class MilkDecorator : public CoffeeDecorator {
public:
MilkDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
std::string getDescription() const override {
return coffee->getDescription() + ", Milk";
}
double getCost() const override { return coffee->getCost() + 0.5; }
};
class SugarDecorator : public CoffeeDecorator {
public:
SugarDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
std::string getDescription() const override {
return coffee->getDescription() + ", Sugar";
}
double getCost() const override { return coffee->getCost() + 0.2; }
};Decorators can be stacked - each one wraps the previous, building up functionality layer by layer:
int main() {
std::unique_ptr<Coffee> order = std::make_unique<SimpleCoffee>();
order = std::make_unique<MilkDecorator>(std::move(order));
order = std::make_unique<SugarDecorator>(std::move(order));
std::cout << order->getDescription() << ": $" << order->getCost();
// Output: Coffee, Milk, Sugar: $2.7
}Use Decorator when you need to add responsibilities to objects dynamically without affecting other objects, or when extending functionality through subclassing would result in an explosion of classes.
Challenge
EasyLet's build a Pizza Order System using the Decorator pattern. You'll create a flexible system where customers can start with a base pizza and add various toppings—each topping wraps the previous order and adds its own cost and description. This is a perfect use case for decorators: toppings can be stacked in any combination without creating an explosion of subclasses.
You'll organize your code across three files:
Pizza.h: Define the component interface and the base pizza class.Create an abstract
Pizzaclass with pure virtual methodsgetDescription()(returns a string) andgetCost()(returns a double), plus a virtual destructor.Then create a concrete
PlainPizzaclass that represents the base pizza. It should return"Pizza"as its description and have a base cost of8.0.ToppingDecorator.h: Build the decorator base class and concrete topping decorators.Create a
ToppingDecoratorclass that inherits fromPizzaand holds astd::unique_ptr<Pizza>to the wrapped component. This serves as the base for all topping decorators.Implement three concrete topping decorators:
CheeseTopping— adds", Cheese"to the description and1.5to the costPepperoniTopping— adds", Pepperoni"to the description and2.0to the costMushroomTopping— adds", Mushrooms"to the description and1.0to the cost
Each decorator should delegate to the wrapped pizza and then add its own contribution.
main.cpp: Create a customized pizza order based on user input.Read three inputs (each will be
yesorno):- Add cheese?
- Add pepperoni?
- Add mushrooms?
Start with a
PlainPizza, then wrap it with the appropriate topping decorators based on the inputs (in the order: cheese, pepperoni, mushrooms). Finally, print the order details in this format:Order: [description] Total: $[cost]
For example, with inputs yes, yes, no:
Order: Pizza, Cheese, Pepperoni
Total: $11.5With inputs yes, no, yes:
Order: Pizza, Cheese, Mushrooms
Total: $10.5With inputs no, no, no:
Order: Pizza
Total: $8With inputs yes, yes, yes:
Order: Pizza, Cheese, Pepperoni, Mushrooms
Total: $12.5Notice how each decorator wraps the previous one, building up both the description and cost layer by layer. You can easily add new toppings by creating new decorator classes without modifying the existing pizza or topping code—that's the beauty of the Decorator pattern.
Cheat sheet
The Decorator pattern allows you to add new behaviors to objects dynamically by wrapping them in special objects called decorators. Unlike inheritance, which adds behavior at compile time, decorators let you extend functionality at runtime without modifying the original class.
The pattern works by having both the original object and decorators implement the same interface. Each decorator holds a reference to a component and adds its own behavior before or after delegating to the wrapped object.
Structure
Component interface: Defines the common interface for both concrete components and decorators.
class Coffee {
public:
virtual std::string getDescription() const = 0;
virtual double getCost() const = 0;
virtual ~Coffee() = default;
};Concrete component: The base object that can be decorated.
class SimpleCoffee : public Coffee {
public:
std::string getDescription() const override { return "Coffee"; }
double getCost() const override { return 2.0; }
};Base decorator: Holds a reference to a component and implements the same interface.
class CoffeeDecorator : public Coffee {
protected:
std::unique_ptr<Coffee> coffee;
public:
CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};Concrete decorators: Add specific behaviors by delegating to the wrapped object and adding their own functionality.
class MilkDecorator : public CoffeeDecorator {
public:
MilkDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
std::string getDescription() const override {
return coffee->getDescription() + ", Milk";
}
double getCost() const override { return coffee->getCost() + 0.5; }
};Stacking Decorators
Decorators can be stacked - each one wraps the previous, building up functionality layer by layer:
std::unique_ptr<Coffee> order = std::make_unique<SimpleCoffee>();
order = std::make_unique<MilkDecorator>(std::move(order));
order = std::make_unique<SugarDecorator>(std::move(order));
std::cout << order->getDescription() << ": $" << order->getCost();
// Output: Coffee, Milk, Sugar: $2.7When to Use
Use Decorator when you need to add responsibilities to objects dynamically without affecting other objects, or when extending functionality through subclassing would result in an explosion of classes.
Try it yourself
#include <iostream>
#include <string>
#include <memory>
#include "Pizza.h"
#include "ToppingDecorator.h"
using namespace std;
int main() {
// Read three inputs (yes or no)
string addCheese, addPepperoni, addMushrooms;
cin >> addCheese;
cin >> addPepperoni;
cin >> addMushrooms;
// TODO: Start with a PlainPizza using std::unique_ptr
// TODO: If addCheese is "yes", wrap the pizza with CheeseTopping
// TODO: If addPepperoni is "yes", wrap the pizza with PepperoniTopping
// TODO: If addMushrooms is "yes", wrap the pizza with MushroomTopping
// TODO: Print the order in the format:
// Order: [description]
// Total: $[cost]
return 0;
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of OOP
External FilesC++ Build & CompilationHeader Files & Source FilesNamespaces & ScopeIntroduction to OOP in C++Classes vs ObjectsThe 'this' PointerMethods (Member Functions)Attributes (Data Members)Ctors & Dtors BasicsRecap - Simple Calculator4Class Properties
Instance vs Static MembersGetters and SettersConst Member FunctionsMutable KeywordStatic Methods and VariablesFriend Functions & ClassesRecap - Bank Account Manager7Inheritance
Basic InheritanceInheritance Access LevelsCtor & Dtor Call OrderMethod OverridingVirtual Functions & VTableMultiple InheritanceVirtual InheritanceRecap - Employee Hierarchy2Memory Management
Stack vs Heap MemoryPointers and ReferencesDynamic Memory (new/delete)Smart Pointers in C++RAII in C++Recap - Dynamic Array Manager5Encapsulation
Access Specifiers in C++Access Specifiers In DepthInformation HidingStruct vs ClassNested & Inner ClassesRecap - Student Records System8Polymorphism
Compile vs Runtime PolymorphFunction OverloadingVirtual Functions RevisitedPure Virtual FunctionsAbstract ClassesInterface Design in C++Dynamic Casting & RTTIRecap - Shape Calculator11Advanced OOP Concepts
Composition vs InheritanceMixins via CRTPPimpl IdiomType ErasureEnum Classes & Strong TypingException Handling in OOPCustom Exception Hierarchies14Design Patterns Part 2
Command PatternAdapter PatternDecorator PatternTemplate Method PatternState PatternComposite PatternRAII as a Pattern3Constructors & Destructors
Default ConstructorParameterized ConstructorCopy ConstructorMove ConstructorConstructor Init ListsDelegating ConstructorsDestructor Deep DiveRule of Three / Five / ZeroRecap - String Class6Operator Overloading
Intro to Operator OverloadArithmetic Operator OverloadComparison Operator OverloadStream OperatorsAssignment Operator Overload[] and () Operator OverloadType Conversion OperatorsRecap - Matrix Class9Templates
Function TemplatesClass TemplatesTemplate SpecializationVariadic TemplatesSFINAE & Type Traits BasicsRecap - Generic Container