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.
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:
Coffee
classMilkDecorator
and SugarDecorator
to add features to the basic coffee objectHere'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:
C++1class Coffee { 2public: 3 virtual std::string getDescription() { 4 return "Coffee"; 5 } 6 virtual double cost() = 0; 7 virtual ~Coffee() = default; 8}; 9 10class SimpleCoffee : public Coffee { 11public: 12 double cost() override { 13 return 2.0; 14 } 15};
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:
C++1class CoffeeDecorator : public Coffee { 2protected: 3 Coffee* decoratedCoffee; 4public: 5 CoffeeDecorator(Coffee* coffee) : decoratedCoffee(coffee) {} 6 std::string getDescription() override { 7 return decoratedCoffee->getDescription(); 8 } 9 virtual ~CoffeeDecorator() { 10 delete decoratedCoffee; 11 } 12};
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:
C++1class MilkDecorator : public CoffeeDecorator { 2public: 3 MilkDecorator(Coffee* coffee) : CoffeeDecorator(coffee) {} 4 std::string getDescription() override { 5 return decoratedCoffee->getDescription() + ", Milk"; 6 } 7 double cost() override { 8 return decoratedCoffee->cost() + 0.5; 9 } 10}; 11 12class SugarDecorator : public CoffeeDecorator { 13public: 14 SugarDecorator(Coffee* coffee) : CoffeeDecorator(coffee) {} 15 std::string getDescription() override { 16 return decoratedCoffee->getDescription() + ", Sugar"; 17 } 18 double cost() override { 19 return decoratedCoffee->cost() + 0.2; 20 } 21};
Let's now see how you can use these classes to create and customize coffee orders using the Decorator Pattern:
C++1int main() { 2 Coffee* coffee = new SimpleCoffee(); 3 std::cout << "Description: " << coffee->getDescription() << ", Cost: " << coffee->cost() << std::endl; // Output: Description: Coffee, Cost: 2 4 5 Coffee* coffeeWithMilk = new MilkDecorator(coffee); 6 std::cout << "Description: " << coffeeWithMilk->getDescription() << ", Cost: " << coffeeWithMilk->cost() << std::endl; // Output: Description: Coffee, Milk, Cost: 2.5 7 8 Coffee* coffeeWithMilkAndSugar = new SugarDecorator(coffeeWithMilk); 9 std::cout << "Description: " << coffeeWithMilkAndSugar->getDescription() << ", Cost: " << coffeeWithMilkAndSugar->cost() << std::endl; // Output: Description: Coffee, Milk, Sugar, Cost: 2.7 10 11 delete coffeeWithMilkAndSugar; 12 return 0; 13}
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:
Coffee
class is the component that defines the basic interface for coffee objects.SimpleCoffee
class is a concrete component that implements the basic coffee object.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.Component
object. In our example, the MilkDecorator
and SugarDecorator
classes are concrete decorators that add milk and sugar to the coffee object, respectively.Let's consider a few scenarios where the Decorator Pattern can be effectively applied:
Let's explore the benefits and drawbacks of using the Decorator Pattern:
Mastering the Decorator Pattern is important because it allows you to extend the functionality of objects at runtime without altering their structure. This leads to more flexible and maintainable code compared to using subclassing for every new feature.
In our coffee example, you can easily add new features like milk and sugar by layering decorators, which can be combined in various ways. Whether you need to add functionality to user interface components, logging systems, or graphical rendering engines, the Decorator Pattern provides a powerful and elegant solution.
Excited to see how this unfolds? Let's get started with the practice section, where you'll implement and extend the Decorator Pattern step-by-step.