Implementing Decorator Design Pattern
Introduction
The Decorator pattern is a design pattern that allows you to add new behaviours or functionalities to an existing object dynamically without modifying its structure. It is part of the structural design patterns in object-oriented programming.
The main idea behind the Decorator pattern is to wrap an object within another object that provides additional behaviours or modifications. This wrapping is achieved by creating a set of decorator classes that implement the same interface as the object being decorated. These decorators add their specific functionalities while delegating the rest of the work to the underlying object.
Main Features:
- Decorator has both Has-A and Is-A relationship with the Base class.
- Prevents Class overloading.
- Becomes easier to add new functionality to existing classes.
Here is an example in C++
#include <iostream>
#include <string>
// Component interface
class Coffee {
public:
virtual double getCost() const = 0;
virtual std::string getDescription() const = 0;
};
// Concrete component
class SimpleCoffee : public Coffee {
public:
double getCost() const override {
return 1.0;
}
std::string getDescription() const override {
return "Simple Coffee";
}
};
// Decorator
class CoffeeDecorator : public Coffee {
protected:
Coffee* coffee;
public:
CoffeeDecorator(Coffee* coffee) : coffee(coffee) {}
double getCost() const override {
return coffee->getCost();
}
std::string getDescription() const override {
return coffee->getDescription();
}
};
// Concrete decorators
class Milk : public CoffeeDecorator {
public:
Milk(Coffee* coffee) : CoffeeDecorator(coffee) {}
double getCost() const override {
return coffee->getCost() + 0.5;
}
std::string getDescription() const override {
return coffee->getDescription() + ", Milk";
}
};
class Sugar : public CoffeeDecorator {
public:
Sugar(Coffee* coffee) : CoffeeDecorator(coffee) {}
double getCost() const override {
return coffee->getCost() + 0.2;
}
std::string getDescription() const override {
return coffee->getDescription() + ", Sugar";
}
};
int main() {
Coffee* coffee = new SimpleCoffee();
std::cout << "Description: " << coffee->getDescription() << std::endl; // Output: Simple Coffee
std::cout << "Cost: $" << coffee->getCost() << std::endl; // Output: $1.0
Coffee* coffeeWithMilk = new Milk(coffee);
std::cout << "Description: " << coffeeWithMilk->getDescription() << std::endl; // Output: Simple Coffee, Milk
std::cout << "Cost: $" << coffeeWithMilk->getCost() << std::endl; // Output: $1.5
Coffee* coffeeWithMilkAndSugar = new Sugar(coffeeWithMilk);
std::cout << "Description: " << coffeeWithMilkAndSugar->getDescription() << std::endl; // Output: Simple Coffee, Milk, Sugar
std::cout << "Cost: $" << coffeeWithMilkAndSugar->getCost() << std::endl; // Output: $1.7
delete coffee;
delete coffeeWithMilk;
delete coffeeWithMilkAndSugar;
return 0;
}
Explanation
This code demonstrates the use of the Decorator design pattern to add new functionalities to an object dynamically. The scenario here is related to coffee and its customization with various ingredients. Let’s break down the code:
1. Coffee Interface (`Coffee`):
This is an abstract class that serves as the interface for the coffee components. It declares two pure virtual functions: getCost() and getDescription(), which must be implemented by concrete coffee classes.
2. Concrete Component (SimpleCoffee):
This is a basic implementation of the Coffee interface. It provides the base functionality of a simple coffee.
3. Decorator Base Class (CoffeeDecorator):
This is an abstract class that also implements the Coffee interface. It has a protected member variable coffee, which is a pointer to the wrapped Coffee object.
The constructor takes a Coffee as a parameter to set the coffee member.
4. Concrete Decorators (Milk and Sugar):
These are concrete implementations of the CoffeeDecorator class.
They override the getCost() and getDescription() functions to add the cost and description of the additional ingredients (milk and sugar).
5. Main Function:
In the main() function, a SimpleCoffee object is created and its description and cost are printed.
Then, a Milk decorator is created, wrapping the SimpleCoffee object. The description and cost of this decorated coffee are printed.
Next, a Sugar decorator is created, wrapping the previously decorated coffee. Again, the description and cost are printed.
Finally, all dynamically allocated objects are deleted to avoid memory leaks.
The Decorator pattern is useful when you want to extend the behaviour of individual objects without modifying their structure. It promotes code flexibility and reusability by allowing you to compose objects with different combinations of behaviours.
I hope this information helps you. Thanks for reading.
Kinshuk