Open In App

Introduction to Decorator Pattern in C++ | Design Patterns

Last Updated : 31 Oct, 2023
Improve
Improve
Like Article
Like
Save
Share
Report

The Decorator Pattern is a structural design pattern in software engineering that enables the dynamic addition of new behaviors or responsibilities to individual objects without altering their underlying class structure. It achieves this by creating a set of decorator classes that are used to wrap concrete components, which represent the core functionality.

Cpp-Design-Pattern

Characteristics of the Decorator Pattern

  • Decorators conform to the same interface as the components they decorate, allowing them to be used interchangeably.
  • This pattern promotes flexibility and extensibility in software systems by allowing developers to compose objects with different combinations of functionalities at runtime.
  • It adheres to the open/closed principle, as new decorators can be added without modifying existing code, making it a powerful tool for building modular and customizable software components.
  • The Decorator Pattern is commonly used in scenarios where a variety of optional features or behaviors need to be added to objects in a flexible and reusable manner, such as in text formatting, graphical user interfaces, or customization of products like coffee or ice cream.

Key Components of the Decorator Pattern

1. Component Interface

This is an abstract class or interface that defines the common interface for both the concrete components and decorators. It specifies the operations that can be performed on the objects.

2. Concrete Component

These are the basic objects or classes that implement the Component interface. They are the objects to which we want to add new behavior or responsibilities.

3. Decorator

This is an abstract class that also implements the Component interface and has a reference to a Component object. Decorators are responsible for adding new behaviors to the wrapped Component object.

4. Concrete Decorator

These are the concrete classes that extend the Decorator class. They add specific behaviors or responsibilities to the Component. Each Concrete Decorator can add one or more behaviors to the Component.

Use Cases for the Decorator Pattern

  • Adding Features to GUI Widgets: We can use the Decorator Pattern to add features like borders, scrollbars, or tooltips to GUI components.
  • Text Formatting: Decorators can be applied to text elements to add formatting such as fonts, colors, or styles.
  • Input/ Output Streams: In C++, decorators can be used to add functionality like buffering, compression, or encryption to input/output streams.
  • Coffee or Food Customization: As shown in previous examples, we can use the Decorator Pattern to customize food items like coffee, cakes, ice creams or pizza by adding toppings, flavors, or decorations.

EXAMPLES of the Decorator Pattern in C++

Example 1:

Below is a C++ code example that demonstrates the Decorator Pattern applied to an ice cream ordering system:

C++




#include <iostream>
#include <string>
 
using namespace std;
 
// Component interface - defines the basic ice cream
// operations.
class IceCream {
public:
    virtual string getDescription() const = 0;
    virtual double cost() const = 0;
};
 
// Concrete Component - the basic ice cream class.
class VanillaIceCream : public IceCream {
public:
    string getDescription() const override
    {
        return "Vanilla Ice Cream";
    }
 
    double cost() const override { return 160.0; }
};
 
// Decorator - abstract class that extends IceCream.
class IceCreamDecorator : public IceCream {
protected:
    IceCream* iceCream;
 
public:
    IceCreamDecorator(IceCream* ic)
        : iceCream(ic)
    {
    }
 
    string getDescription() const override
    {
        return iceCream->getDescription();
    }
 
    double cost() const override
    {
        return iceCream->cost();
    }
};
 
// Concrete Decorator - adds chocolate topping.
class ChocolateDecorator : public IceCreamDecorator {
public:
    ChocolateDecorator(IceCream* ic)
        : IceCreamDecorator(ic)
    {
    }
 
    string getDescription() const override
    {
        return iceCream->getDescription()
               + " with Chocolate";
    }
 
    double cost() const override
    {
        return iceCream->cost() + 100.0;
    }
};
 
// Concrete Decorator - adds caramel topping.
class CaramelDecorator : public IceCreamDecorator {
public:
    CaramelDecorator(IceCream* ic)
        : IceCreamDecorator(ic)
    {
    }
 
    string getDescription() const override
    {
        return iceCream->getDescription() + " with Caramel";
    }
 
    double cost() const override
    {
        return iceCream->cost() + 150.0;
    }
};
 
int main()
{
    // Create a vanilla ice cream
    IceCream* vanillaIceCream = new VanillaIceCream();
    cout << "Order: " << vanillaIceCream->getDescription()
         << ", Cost: Rs." << vanillaIceCream->cost()
         << endl;
 
    // Wrap it with ChocolateDecorator
    IceCream* chocolateIceCream
        = new ChocolateDecorator(vanillaIceCream);
    cout << "Order: " << chocolateIceCream->getDescription()
         << ", Cost: Rs." << chocolateIceCream->cost()
         << endl;
 
    // Wrap it with CaramelDecorator
    IceCream* caramelIceCream
        = new CaramelDecorator(chocolateIceCream);
    cout << "Order: " << caramelIceCream->getDescription()
         << ", Cost: Rs." << caramelIceCream->cost()
         << endl;
 
    delete vanillaIceCream;
    delete chocolateIceCream;
    delete caramelIceCream;
 
    return 0;
}


Output

Order: Vanilla Ice Cream, Cost: Rs.160
Order: Vanilla Ice Cream with Chocolate, Cost: Rs.260
Order: Vanilla Ice Cream with Chocolate with Caramel, Cost: Rs.410








Explanation of the above code:

  • We start by defining a ‘IceCream‘ class, which serves as the component interface.
    • This class defines two pure virtual methods: ‘getDescription()‘ and ‘cost()‘.
    • These methods represent the basic operations that any type of ice cream should have.
  • Next, we create a concrete component called ‘VanillaIceCream‘ that inherits from ‘IceCream‘.
    • This class provides the base implementation of vanilla ice cream, including a description (“Vanilla Ice Cream”) and a base cost of Rs.160.
  • We define an abstract decorator class called ‘IceCreamDecorator‘, which also implements the ‘IceCream‘ interface.
    • This class has a protected member variable ‘iceCream‘ of type ‘IceCream*‘.
    • It acts as a wrapper for ice cream objects and delegates the ‘getDescription()‘ and ‘cost()‘ methods to the wrapped ice cream.
  • Two concrete decorator classes, ‘ChocolateDecorator‘ and ‘CaramelDecorator‘, are created.
    • These classes inherit from ‘IceCreamDecorator‘.
    • Each of them takes an ‘IceCream*‘ as a parameter in their constructors, allowing them to wrap other ice cream objects.

In the ‘main()‘ function, we demonstrate the use of decorators to customize an ice cream order:

  • We start by creating a ‘VanillaIceCream‘ object and print its description and cost.
  • Then, we wrap it with a ‘ChocolateDecorator‘ to add chocolate topping and print the updated description and cost.
  • Finally, we wrap the chocolate ice cream with a ‘CaramelDecorator‘ to add caramel topping and print the final description and cost.
  • After each ice cream object is created and used, we delete them to release the allocated memory.

decorator-pattern-Cpp-1

Output Explanation:

  • Initially, we have a “Vanilla Ice Cream” with a cost of Rs.160.00.
  • After adding a “ChocolateDecorator,” the ice cream becomes “Vanilla Ice Cream with Chocolate” with a cost of Rs.260.00 (an additional Rs.100.00 for chocolate topping).
  • Finally, when we add a “CaramelDecorator,” the ice cream becomes “Vanilla Ice Cream with Chocolate with Caramel” with a total cost of Rs.410.00 (an additional Rs.150.00 for caramel topping).

Example 2:

Below is an another example that demonstrates the Decorator Pattern for customizing cake orders with various toppings and decorations:

C++




#include <iostream>
#include <string>
 
using namespace std;
 
// Component interface - defines the basic cake operations.
class Cake {
public:
    virtual string getDescription() const = 0;
    virtual double cost() const = 0;
};
 
// Concrete Component - the basic cake class.
class PlainCake : public Cake {
public:
    string getDescription() const override
    {
        return "Plain Cake";
    }
 
    double cost() const override { return 300.0; }
};
 
// Decorator - abstract class that extends Cake.
class CakeDecorator : public Cake {
protected:
    Cake* cake;
 
public:
    CakeDecorator(Cake* c)
        : cake(c)
    {
    }
 
    string getDescription() const override
    {
        return cake->getDescription();
    }
 
    double cost() const override { return cake->cost(); }
};
 
// Concrete Decorator - adds chocolate topping.
class ChocolateDecorator : public CakeDecorator {
public:
    ChocolateDecorator(Cake* c)
        : CakeDecorator(c)
    {
    }
 
    string getDescription() const override
    {
        return cake->getDescription() + " with Chocolate";
    }
 
    double cost() const override
    {
        return cake->cost() + 200.0;
    }
};
 
// Concrete Decorator - adds fruit decorations.
class FruitDecorator : public CakeDecorator {
public:
    FruitDecorator(Cake* c)
        : CakeDecorator(c)
    {
    }
 
    string getDescription() const override
    {
        return cake->getDescription() + " with Fruits";
    }
 
    double cost() const override
    {
        return cake->cost() + 150.0;
    }
};
 
int main()
{
    // Create a plain cake
    Cake* plainCake = new PlainCake();
    cout << "Plain Cake:" << plainCake->getDescription()
         << "\nCost:Rs." << plainCake->cost() << endl;
 
    // Wrap it with ChocolateDecorator
    Cake* chocolateCake = new ChocolateDecorator(plainCake);
    cout << "\nChocolate Cake:"
         << chocolateCake->getDescription() << "\nCost:Rs."
         << chocolateCake->cost() << endl;
 
    // Wrap it with FruitDecorator
    Cake* fruitCake1 = new FruitDecorator(chocolateCake);
    cout << "\nFruit Cake:" << fruitCake1->getDescription()
         << "\nCost:Rs." << fruitCake1->cost() << endl;
 
    // Clean up memory
    delete chocolateCake;
    delete fruitCake1;
 
    // Wrap plain cake with FruitDecorator
    Cake* fruitCake2 = new FruitDecorator(plainCake);
    cout << "\nFruit Cake:" << fruitCake2->getDescription()
         << "\nCost:Rs." << fruitCake2->cost() << endl;
 
    // Clean up memory
    delete plainCake;
    delete fruitCake2;
 
    return 0;
}


Output

Plain Cake:Plain Cake
Cost:Rs.300

Chocolate Cake:Plain Cake with Chocolate
Cost:Rs.500

Fruit Cake:Plain Cake with Chocolate with Fruits
Cost:Rs.650

Fruit Cake:Plain Cake with Fruits
Cost:Rs.450








Explanation of the above code:

  • We start by defining a ‘Cake‘ class, which serves as the component interface.
    • This class defines two pure virtual methods: ‘getDescription()‘ and ‘cost()‘.
    • These methods represent the basic operations that any type of cake should have.
  • Next, we create a concrete component called ‘PlainCake‘ that inherits from ‘Cake‘.
    • This class provides the base implementation of a plain cake, including a description (“Plain Cake”) and a base cost of Rs. 300.00.
  • We define an abstract decorator class called ‘CakeDecorator‘, which also implements the ‘Cake‘ interface.
    • This class has a protected member variable cake of type ‘Cake*‘.
    • It acts as a wrapper for ‘cake‘ objects and delegates the ‘getDescription()‘ and ‘cost()‘ methods to the wrapped cake.
  • Two concrete decorator classes, ‘ChocolateDecorator‘ and ‘FruitDecorator‘, are implemented.
    • Both inherit from ‘CakeDecorator‘. They take a ‘Cake*‘ parameter in their constructors, allowing them to wrap other cake objects.

In the ‘main()‘ function:

  • We start by creating a ‘PlainCake‘ object and print its description and cost.
  • Then, we wrap it with a ‘ChocolateDecorator‘ to add chocolate topping and print the updated description and cost.
  • Next, we wrap the chocolate cake with a ‘FruitDecorator‘ to add fruit decorations and print the final description and cost.
  • After each cake object is created and used, we delete them to release the allocated memory.

decorator-pattern-Cpp--2
Output Explanation:

  • We start with a “Plain Cake” with a cost of Rs. 300.00.
  • After adding a “ChocolateDecorator,” the cake becomes “Plain Cake with Chocolate” with a cost of Rs. 500.00 (base cost + Rs. 200.00 for chocolate topping).
  • Then, when we wrap the chocolate cake with a “FruitDecorator,” the final cake is “Plain Cake with Chocolate with Fruits” with a total cost of Rs. 650.00 (previous cost + Rs. 150.00 for fruit decorations).
  • We delete the ‘chocolateCake‘ and ‘fruitCake1‘ objects to clean up memory.
  • Finally, we create another cake, starting with a ‘PlainCake‘ and directly adding a ‘FruitDecorator‘ to it. This creates a “Plain Cake with Fruits” with a cost of Rs. 450.00 (base cost + Rs. 150.00 for fruit decorations).
  • We delete the ‘plainCake‘ object to clean up memory.

These examples demonstrates how the Decorator Pattern allows us to dynamically add and combine features or toppings to a base object (in this case, ice cream and cake) without altering its underlying class, providing flexibility and extensibility in software design.

Advantages of the Decorator Pattern in C++ Design Patterns

The decorator pattern is a structural design pattern that allows us to add behavior to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class. It is often used to extend the functionality of classes in a flexible and reusable way. Here are some of the advantages of the decorator pattern:

1. Open-Closed Principle:

The decorator pattern follows the open-closed principle, which states that classes should be open for extension but closed for modification. This means you can introduce new functionality to an existing class without changing its source code.

2. Flexibility:

It allows you to add or remove responsibilities (i.e., behaviors) from objects at runtime. This flexibility makes it easy to create complex object structures with varying combinations of behaviors.

3. Reusable Code:

Decorators are reusable components. You can create a library of decorator classes and apply them to different objects and classes as needed, reducing code duplication.

4. Composition over Inheritance:

Unlike traditional inheritance, which can lead to a deep and inflexible class hierarchy, the decorator pattern uses composition. You can compose objects with different decorators to achieve the desired functionality, avoiding the drawbacks of inheritance, such as tight coupling and rigid hierarchies.

5. Dynamic Behavior Modification:

Decorators can be applied or removed at runtime, providing dynamic behavior modification for objects. This is particularly useful when you need to adapt an object’s behavior based on changing requirements or user preferences.

6. Clear Code Structure:

The Decorator pattern promotes a clear and structured design, making it easier for developers to understand how different features and responsibilities are added to objects.

Disadvantages of the Decorator Pattern in C++ Design Patterns

While the Decorator pattern offers several advantages, it also has some disadvantages and trade-offs to consider when deciding whether to use it in a particular software design. Here are some of the disadvantages of the Decorator pattern:

1. Complexity:

As you add more decorators to an object, the code can become more complex and harder to understand. The nesting of decorators can make the codebase difficult to navigate and debug, especially when there are many decorators involved.

2. Increased Number of Classes:

When using the Decorator pattern, you often end up with a large number of small, specialized decorator classes. This can lead to a proliferation of classes in your codebase, which may increase maintenance overhead.

3. Order of Decoration:

The order in which decorators are applied can affect the final behavior of the object. If decorators are not applied in the correct order, it can lead to unexpected results. Managing the order of decorators can be challenging, especially in complex scenarios.

4. Potential for Overuse:

Because it’s easy to add decorators to objects, there is a risk of overusing the Decorator pattern, making the codebase unnecessarily complex. It’s important to use decorators judiciously and only when they genuinely add value to the design.

5. Not Suitable for Every Situation:

The Decorator pattern is best suited for situations where you need to add responsibilities to objects dynamically. In cases where the object structure is relatively stable and changes are infrequent, using other design patterns or techniques might be more efficient and straightforward.

6. Limited Support in Some Languages:

Some programming languages may not provide convenient support for implementing decorators. Implementing the pattern can be more verbose and less intuitive in such languages.



Like Article
Suggest improvement
Previous
Next
Share your thoughts in the comments

Similar Reads