Decorator Design Pattern for Flexible and Dynamic Object Behavior in C++
In this post, we explore the Decorator Design Pattern, a powerful structural pattern that allows you to dynamically add behavior to objects without modifying their code. Through a practical example, we demonstrate how to use decorators to extend object functionality in a flexible and reusable way. This pattern promotes clean design principles like the Open/Closed Principle, ensuring that objects can be extended while keeping existing code intact. Learn how to implement decorators, manage complexity, and enhance your system's flexibility.
Overview
The Decorator Pattern is a structural design pattern that allows behavior to be dynamically added to an individual object, either statically or dynamically, without altering the behavior of other objects from the same class. It is used to extend the functionalities of objects in a flexible and reusable way.
Key Characteristics
- Dynamically Add Functionality: The pattern provides an alternative to subclassing for extending functionality.
- Open/Closed Principle: The pattern adheres to the Open/Closed Principle by allowing objects to be open for extension but closed for modification.
- Compositional Approach: It uses composition instead of inheritance, where decorators “wrap” the original object to provide additional behavior.
Structure
The pattern typically includes the following components:
- Component: An interface or abstract class defining the base functionality.
- Concrete Component: A class that implements the
Component
interface and provides the default behavior. - Decorator: An abstract class that implements the
Component
interface and has a reference to aComponent
object. It forwards requests to the wrapped object while adding new behavior. - Concrete Decorator: A class that extends the
Decorator
class and adds additional functionality.
UML Representation
Component
Interface:- Defines the base functionality.
- Represented as an interface in UML.
ConcreteComponent
Class:- Implements the
Component
interface. - Provides the core functionality.
- Implements the
Decorator
Abstract Class:- Implements the
Component
interface. - Contains a reference to a
Component
object (composition). - Adds functionality by delegating calls to the wrapped component.
- Implements the
- Concrete Decorators (
ConcreteDecoratorA
andConcreteDecoratorB
):- Extend the
Decorator
class. - Add specific behavior while maintaining the base functionality.
- Extend the
The o--
relationship between Decorator
and Component
indicates a composition, showing that the Decorator
wraps a Component
object.
When to Use
- To dynamically and transparently add responsibilities to individual objects without impacting others.
- When subclassing becomes impractical due to the need for numerous independent extensions or combinations of functionality.
The decorator pattern is widely used in UI frameworks, logging systems, and data formatting tasks. It provides a powerful tool for achieving clean and extensible designs.
Advantages
- Dynamic Flexibility: You can add or remove responsibilities at runtime.
- Reusability: Decorators can be applied to multiple objects, promoting code reuse.
- Adherence to Single Responsibility Principle: Each decorator focuses on a specific concern, keeping functionality modular.
- Simplified Class Hierarchies: Reduces the need for a large number of subclasses.
Disadvantages
- Increased Complexity: Managing many small decorator classes can make the design harder to read and understand.
- Performance Overhead: Layering multiple decorators increases the number of objects and can impact performance.
- Hard to Debug: Debugging might be challenging when many decorators are chained.
Coffee Shop Example Using the Decorator Pattern
This example demonstrates how to design a flexible coffee shop application using the Decorator Design Pattern. The pattern is used to dynamically add condiments (like Milk, Mocha, and Soya) to various coffee types (Espresso, DarkRoast, and HouseBlend) without modifying the base classes.
Design Components
- Component
- The
Beverage
abstract class defines the interface for all coffee types and condiments.
- The
- Concrete Components
Espresso
,DarkRoast
, andHouseBlend
classes implementBeverage
and provide specific coffee types.
- Decorator Base Class
- The
ICondimentDecorator
abstract class inherits fromBeverage
and contains a reference to aBeverage
object it decorates.
- The
- Concrete Decorators
Milk
,Mocha
, andSoya
extendICondimentDecorator
, adding specific behaviors and costs.
Class Diagram
Below is the class diagram for the Coffee Shop example:
This diagram illustrates the relationships between the component (Beverage
), the concrete components (Espresso
, DarkRoast
, HouseBlend
), the decorator base class (ICondimentDecorator
), and the concrete decorators (Mocha
, Milk
, Soya
).
Code Implementation
1. Beverage Base Class
Defines the interface for all beverages:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#pragma once
#include <string>
namespace sb {
class Beverage {
public:
virtual ~Beverage() = default;
virtual std::string GetDescription() const { return m_description; }
virtual double GetCost() const = 0;
protected:
std::string m_description = "Unknown Beverage";
};
} // namespace sb
2. Concrete Components
Specific coffee types that extend Beverage
:
Espresso:
1
2
3
4
5
6
7
8
9
10
11
#pragma once
#include "beverage.hpp"
namespace sb {
class Espresso : public Beverage {
public:
Espresso() { m_description = "Espresso"; }
double GetCost() const override { return 1.99; }
};
} // namespace sb
DarkRoast:
1
2
3
4
5
6
7
8
9
10
11
#pragma once
#include "beverage.hpp"
namespace sb {
class DarkRoast : public Beverage {
public:
DarkRoast() { m_description = "Dark Roast Coffee"; }
double GetCost() const override { return 1.99; }
};
} // namespace sb
HouseBlend:
1
2
3
4
5
6
7
8
9
10
11
#pragma once
#include "beverage.hpp"
namespace sb {
class HouseBlend : public Beverage {
public:
HouseBlend() { m_description = "House Blend Coffee"; }
double GetCost() const override { return 1.89; }
};
} // namespace sb
3. Decorator Base Class
Defines the structure for all decorators:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#pragma once
#include "beverage.hpp"
#include <memory>
namespace sb {
class ICondimentDecorator : public Beverage {
public:
ICondimentDecorator(std::shared_ptr<Beverage> beverage) : m_beverage(std::move(beverage)) {}
virtual ~ICondimentDecorator() = default;
virtual std::string GetDescription() const = 0;
protected:
std::shared_ptr<Beverage> m_beverage;
};
} // namespace sb
4. Concrete Decorators
Each condiment dynamically adds behavior and cost:
Milk:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#pragma once
#include "icondiment_decorator.hpp"
namespace sb {
class Milk : public ICondimentDecorator {
public:
Milk(std::shared_ptr<Beverage> beverage) : ICondimentDecorator(std::move(beverage)) {}
std::string GetDescription() const override { return m_beverage->GetDescription() + m_description; }
double GetCost() const override { return m_beverage->GetCost() + m_cost; }
private:
std::string m_description = ", Milk";
const double m_cost = 0.10;
};
}
Mocha:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#pragma once
#include "icondiment_decorator.hpp"
namespace sb {
class Mocha : public ICondimentDecorator {
public:
Mocha(std::shared_ptr<Beverage> beverage) : ICondimentDecorator(std::move(beverage)) {}
std::string GetDescription() const override {
return m_beverage->GetDescription() + m_description;
}
double GetCost() const override { return m_beverage->GetCost() + m_cost; }
private:
std::string m_description = ", Mocha";
const double m_cost = 0.20;
};
}
Soya:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#pragma once
#include "icondiment_decorator.hpp"
namespace sb {
class Soya : public ICondimentDecorator {
public:
Soya(std::shared_ptr<Beverage> beverage) : ICondimentDecorator(std::move(beverage)) {}
std::string GetDescription() const override {
return m_beverage->GetDescription() + m_description;
}
double GetCost() const override { return m_beverage->GetCost() + m_cost; }
private:
std::string m_description = ", Soya";
const double m_cost = 0.15;
};
} // namespace sb
5. Main Program
Demonstrates dynamically decorating beverages:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include "beverage.hpp"
#include "dark_roast.hpp"
#include "espresso.hpp"
#include "house_blend.hpp"
#include "milk.hpp"
#include "mocha.hpp"
#include "soya.hpp"
#include <iostream>
#include <memory>
int main() {
using namespace sb;
// Plain Espresso
std::shared_ptr<Beverage> beverage = std::make_shared<Espresso>();
std::cout << beverage->GetDescription() << " £" << beverage->GetCost() << std::endl;
// Dark Roast with double Mocha and Milk
std::shared_ptr<Beverage> beverage2 = std::make_shared<DarkRoast>();
beverage2 = std::make_shared<Mocha>(beverage2);
beverage2 = std::make_shared<Mocha>(beverage2);
beverage2 = std::make_shared<Milk>(beverage2);
std::cout << beverage2->GetDescription() << " £" << beverage2->GetCost() << std::endl;
// House Blend with Soya, Mocha, and Milk
std::shared_ptr<Beverage> beverage3 = std::make_shared<HouseBlend>();
beverage3 = std::make_shared<Soya>(beverage3);
beverage3 = std::make_shared<Mocha>(beverage3);
beverage3 = std::make_shared<Milk>(beverage3);
std::cout << beverage3->GetDescription() << " £" << beverage3->GetCost() << std::endl;
return 0;
}
Output
1
2
3
Espresso £1.99
Dark Roast Coffee, Mocha, Mocha, Milk £2.49
House Blend Coffee, Soya, Mocha, Milk £2.34
This demonstrates how the Decorator Pattern enables dynamic extension of functionality without modifying existing code. Each condiment is a reusable, composable component.
Conclusion
The Decorator Design Pattern is a powerful tool for extending the functionality of objects dynamically and transparently, without altering their structure or relying on an extensive inheritance hierarchy. In the Coffee Shop example, it allows us to dynamically add condiments like Milk, Mocha, and Soya to beverages such as Espresso, DarkRoast, and HouseBlend. This approach adheres to the Open/Closed Principle, ensuring that the system is open for extension but closed for modification.
By separating the core functionality (coffee types) from additional behaviors (condiments), the pattern promotes flexibility, reusability, and modularity. However, as demonstrated, the use of multiple decorators can introduce complexity, which should be carefully managed in larger systems.
Overall, the Decorator Pattern is ideal when you need a scalable and maintainable solution to dynamically combine behaviors, as it strikes a balance between flexibility and adherence to solid design principles.