Open In App

Introduction to Decorator Pattern in C++ | Design Patterns

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.



Characteristics of the Decorator Pattern

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

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:




#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:

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

Output Explanation:

Example 2:

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




#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:

In the ‘main()‘ function:


Output Explanation:

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.


Article Tags :