Menu
Coddy logo textTech

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 icon

Challenge

Easy

Let'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 Pizza class with pure virtual methods getDescription() (returns a string) and getCost() (returns a double), plus a virtual destructor.

    Then create a concrete PlainPizza class that represents the base pizza. It should return "Pizza" as its description and have a base cost of 8.0.

  • ToppingDecorator.h: Build the decorator base class and concrete topping decorators.

    Create a ToppingDecorator class that inherits from Pizza and holds a std::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 and 1.5 to the cost
    • PepperoniTopping — adds ", Pepperoni" to the description and 2.0 to the cost
    • MushroomTopping — adds ", Mushrooms" to the description and 1.0 to 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 yes or no):

    1. Add cheese?
    2. Add pepperoni?
    3. 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.5

With inputs yes, no, yes:

Order: Pizza, Cheese, Mushrooms
Total: $10.5

With inputs no, no, no:

Order: Pizza
Total: $8

With inputs yes, yes, yes:

Order: Pizza, Cheese, Pepperoni, Mushrooms
Total: $12.5

Notice 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.7

When 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;
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming