Exploring the Decorator Pattern

Exploring the Decorator Pattern

Welcome back! Having learned about the Adapter and Composite Patterns, you're well on your way to mastering structural design patterns in C++. In this lesson, we'll dive into the Decorator Pattern, another important structural pattern that allows us to add new functionalities to objects dynamically and transparently. This is particularly useful when you want to enhance the behavior of objects without modifying their code.

What You'll Learn

The Decorator Pattern enables you to wrap an object with additional behavior in a flexible and reusable way. You'll learn how to use this pattern to extend the functionality of objects in a clean and maintainable manner. For example, let's consider a simple coffee ordering system. By the end of this lesson, you will be able to:

  • Create a basic Coffee class
  • Implement decorators like MilkDecorator and SugarDecorator to add features to the basic coffee object

Here's a snippet from the code you'll be working with:

We start by defining a basic Coffee class with a getDescription method that returns the name of the coffee and a cost method that returns the price of the coffee:

class Coffee {
public:
    virtual std::string getDescription() {
        return "Coffee";
    }
    virtual double cost() = 0;
    virtual ~Coffee() = default;
};

class SimpleCoffee : public Coffee {
public:
    double cost() override {
        return 2.0;
    }
};

Next, we define a CoffeeDecorator class that extends the Coffee class and contains a pointer to the decorated coffee object. This serves as the base class for all decorators that add new features to the coffee object:

class CoffeeDecorator : public Coffee {
protected:
    Coffee* decoratedCoffee;
public:
    CoffeeDecorator(Coffee* coffee) : decoratedCoffee(coffee) {}
    std::string getDescription() override {
        return decoratedCoffee->getDescription();
    }
    virtual ~CoffeeDecorator() {
        delete decoratedCoffee;
    }
};

Finally, we implement concrete decorators like MilkDecorator and SugarDecorator that add milk and sugar to the coffee, respectively. These decorators extend the functionality of the decorated coffee object by adding new features:

class MilkDecorator : public CoffeeDecorator {
public:
    MilkDecorator(Coffee* coffee) : CoffeeDecorator(coffee) {}
    std::string getDescription() override {
        return decoratedCoffee->getDescription() + ", Milk";
    }
    double cost() override {
        return decoratedCoffee->cost() + 0.5;
    }
};

class SugarDecorator : public CoffeeDecorator {
public:
    SugarDecorator(Coffee* coffee) : CoffeeDecorator(coffee) {}
    std::string getDescription() override {
        return decoratedCoffee->getDescription() + ", Sugar";
    }
    double cost() override {
        return decoratedCoffee->cost() + 0.2;
    }
};

Let's now see how you can use these classes to create and customize coffee orders using the Decorator Pattern:

int main() {
    Coffee* coffee = new SimpleCoffee();
    std::cout << "Description: " << coffee->getDescription() << ", Cost: " << coffee->cost() << std::endl; // Output: Description: Coffee, Cost: 2

    Coffee* coffeeWithMilk = new MilkDecorator(coffee);
    std::cout << "Description: " << coffeeWithMilk->getDescription() << ", Cost: " << coffeeWithMilk->cost() << std::endl; // Output: Description: Coffee, Milk, Cost: 2.5

    Coffee* coffeeWithMilkAndSugar = new SugarDecorator(coffeeWithMilk);
    std::cout << "Description: " << coffeeWithMilkAndSugar->getDescription() << ", Cost: " << coffeeWithMilkAndSugar->cost() << std::endl; // Output: Description: Coffee, Milk, Sugar, Cost: 2.7

    delete coffeeWithMilkAndSugar;
    return 0;
}

Notice, how decorators like MilkDecorator and SugarDecorator can be combined to create customized coffee orders. This allows you to add new features to the coffee object at runtime without modifying its code.

Let's now break down the key components of the Decorator Pattern:

  • Component: Defines the interface for objects that can have responsibilities added to them dynamically. In our example, the Coffee class is the component that defines the basic interface for coffee objects.
  • ConcreteComponent: Represents the basic object to which additional responsibilities can be added. In our example, the SimpleCoffee class is a concrete component that implements the basic coffee object.
  • Decorator: Maintains a reference to a Component object and defines an interface that conforms to the Component interface. In our example, the CoffeeDecorator class is the decorator that extends the functionality of the Coffee object.
  • ConcreteDecorator: Adds new responsibilities to the Component object. In our example, the MilkDecorator and SugarDecorator classes are concrete decorators that add milk and sugar to the coffee object, respectively.
Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal