Design Patterns in C++
Design Patterns in C++
Design Patterns
IN C++
A Comprehensive Guide to
Modern Software Development
Written by
Ray
sderay.com
Connect with SDE Ray
Stay connected with SDE Ray, the author of Mastering Design Patterns in C++. Follow me on these
platforms to explore insights into software development, design patterns, and the art of clean code.
Engage with my posts, share your thoughts, and join the conversation!
Website 🌐
Visit my official website for detailed blogs, tutorials, and resources on software development:
sderay.com
𝕏
Follow me on X (formerly Twitter) for quick insights, coding tips, and updates: @sde_ray
Instagram
Join me on Instagram for a behind-the-scenes look at my coding journey, snapshots of my work,
and more: @sde.ray
Note:
Some additional behavioral patterns will be added in future versions to further enrich your understanding and capabilities.
Design Patterns in C++
What Are Design Patterns?
Design patterns are tried-and-tested solutions to common problems that developers face when building software. Think of them as best practices or templates that
you can follow to solve specific coding challenges. By using design patterns, you can write code that's easier to understand, maintain, and reuse.
At their core, design patterns are reusable templates that empower developers to address recurring challenges in software design. They offer a structured approach,
promoting adaptability for specific design problems.
Types of Design Patterns
Design patterns are grouped into three main categories based on what they focus on:
1. Creational Patterns: How Objects Are Created
These patterns are all about making sure objects are created in the best possible way for your situation. Instead of just using new to create an object, creational
patterns give you more control over the creation process. They help you manage how objects are made, ensuring your code is flexible and can easily adapt to
changes.
2. Structural Patterns: How Objects Are Organized
Structural patterns focus on how objects and classes fit together to form larger structures. These patterns are about making sure that if one part of your system
changes, the rest doesn’t need to. They help you compose objects and classes in a way that makes your system more flexible and easier to understand.
3. Behavioral Patterns: How Objects Communicate
Behavioral patterns deal with how objects interact and communicate with each other. These patterns help define clear roles and responsibilities for objects, making
sure your system behaves correctly and is easy to manage and extend.
Explore Individual Design Patterns
Each design pattern solves a specific problem in a certain way. Below are the types of patterns, with links to detailed explanations and examples of each:
Creational Patterns
Singleton Pattern: Ensures there’s only one instance of a class and provides a global access point to it.
Prototype Pattern: Allows you to create new objects by copying an existing object (a prototype).
Factory Method Pattern: Lets you create objects without specifying the exact class of the object that will be created.
Abstract Factory Pattern: Lets you create families of related objects without specifying their concrete classes.
Builder Pattern: Helps you construct complex objects step by step, separating the construction process from the final representation.
Structural Patterns
Adapter Pattern: Allows two incompatible interfaces to work together.
Composite Pattern: Lets you compose objects into tree structures to represent part-whole hierarchies.
Decorator Pattern: Adds new responsibilities to an object dynamically, without altering its structure.
Facade Pattern: Provides a simple interface to a complex system, making it easier to use.
Flyweight Pattern: Reduces memory usage by sharing as much data as possible with similar objects.
Proxy Pattern: Provides a placeholder or proxy for another object to control access to it.
Behavioral Patterns
Chain of Responsibility Pattern: Passes a request along a chain of handlers, where each handler decides whether to process the request or pass it along.
Command Pattern: Encapsulates a request as an object, allowing you to parameterize and queue requests.
Iterator Pattern: Provides a way to access the elements of a collection sequentially without exposing its underlying representation.
Mediator Pattern: Defines an object that manages communication between other objects, promoting loose coupling.
Memento Pattern: Captures and restores an object’s internal state without breaking encapsulation.
Observer Pattern: Notifies all dependent objects when an object’s state changes, automatically updating them.
State Pattern: Allows an object to alter its behavior when its internal state changes.
Strategy Pattern: Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
Template Method Pattern: Defines the steps of an algorithm, allowing subclasses to provide their own implementation for some steps.
Visitor Pattern: Let you add further operations to objects without modifying them.
Why Use Design Patterns?
Consistency: Ensures that your code follows best practices.
Reusability: Patterns are designed to solve common problems, so you can reuse them in different parts of your application.
1
Scalability: Helps your code adapt to changes and grow as your project evolves.
Contributing
I welcome contributions! If you have a new pattern, improvement, or a different approach to an existing pattern, feel free to email me at sderay.mail@gmail.com with
the subject Design Pattern Suggestion or by tagging me on 𝕏(twitter): @sde_ray. Let's build the most comprehensive C++ design patterns resource together!
2
Singleton Design Pattern
Introduction
The Singleton Design Pattern is a creational pattern that ensures a class has only one instance while providing a global point of access to that instance. This pattern
is widely used when a single object is needed to coordinate actions across the entire system, such as managing a database connection, logging system, or
configuration settings.
Key Concepts
Single Instance: The class ensures that only one instance is created, preventing the creation of multiple instances.
Global Access: The Singleton instance is accessible globally, allowing various parts of the application to interact with it.
Private Constructor: The constructor is private or protected to prevent direct instantiation from outside the class.
When to Use the Singleton Pattern
When you need to ensure there is exactly one instance of a class.
When the single instance should be accessible globally across different parts of the application.
When you want to control access to a shared resource, such as logging, configuration, or database connections.
Example in C++
Implementation Overview
Below is a simple implementation of the Singleton Design Pattern in C++.
#include <iostream>
#include <mutex>
class Singleton {
private:
static Singleton* instance; // Static instance pointer
static std::mutex mtx; // Mutex for thread safety
public:
// Static method to get the single instance of the class
static Singleton* getInstance() {
std::lock_guard<std::mutex> lock(mtx); // Ensure thread safety
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
void showMessage() {
std::cout << "Hello from Singleton!" << std::endl;
}
};
int main() {
// Get the singleton instance and use it
Singleton* singleton1 = Singleton::getInstance();
singleton1->showMessage();
3
return 0;
}
How It Works
Private Constructor: The constructor is private, preventing other classes from creating instances directly.
Static Instance: A static pointer ( instance ) holds the single instance of the class.
Thread Safety: The std::mutex and std::lock_guard ensure that only one thread can create the instance at a time, preventing multiple instances in a
multithreaded environment.
No Copying: The copy constructor and assignment operator are deleted to prevent copying the singleton instance.
Benefits of the Singleton Pattern
Controlled Access: The pattern controls how and when the instance is created and accessed.
Memory Efficiency: Since only one instance is created, memory usage is optimized.
Consistency: A single instance ensures consistent behavior across the application.
Common Pitfalls
Global State: Excessive use of the Singleton pattern can lead to hidden dependencies and make the codebase harder to understand and test.
Thread Safety: In multithreaded environments, you must ensure that the Singleton instance is created safely. This example uses a mutex for thread safety.
Conclusion
The Singleton Design Pattern is a powerful tool when you need a single, globally accessible instance of a class. It provides both control and consistency in object
creation, making it ideal for managing shared resources like configurations, logging systems, or database connections.
4
Prototype Design Pattern
Introduction
The Prototype design pattern is a creational pattern that allows you to create new objects by copying or cloning an existing object, known as the prototype. This
pattern is useful when creating objects is resource-intensive, and you want to avoid the overhead of initializing new objects from scratch. Instead of creating ojbects
from scratch, this pattern creates similar object by cloing existing ones. The clone perfroms a Deep Copy, hence any changes made to the clone oject doesn't change
the value of original object.
When to Use the Prototype Pattern
When object creation is costly (in terms of time or resources).
When the system should be independent of how its objects are created, composed, and represented.
When you need to create objects that are similar but not exactly the same.
Structure of the Prototype Pattern
The pattern typically involves the following components:
1. Prototype Interface: Declares a cloning method.
2. Concrete Prototype: Implements the cloning method. This class defines the object that will be copied.
3. Client: Creates a new object by requesting a clone from the Prototype.
Example in C++
Here's a simple example of how you might implement the Prototype pattern in C++:
#include <iostream>
#include <unordered_map>
// Prototype Interface
class Shape {
public:
virtual Shape* clone() const = 0;
virtual void draw() const = 0;
virtual void modify(int newValue1, int newValue2 = 0) = 0;
virtual ~Shape() {}
};
// Client
class PrototypeFactory {
private:
std::unordered_map<std::string, Shape*> prototypes;
public:
5
PrototypeFactory() {
prototypes["Circle"] = new Circle(10);
prototypes["Rectangle"] = new Rectangle(5, 7);
}
~PrototypeFactory() {
// Delete all prototypes to avoid memory leaks
for (auto& pair : prototypes) {
delete pair.second;
}
}
};
int main() {
PrototypeFactory factory;
return 0;
}
Explanation
Prototype Interface ( Shape ): This abstract class declares the clone() method that returns a Shape* , allowing objects to be cloned.
Concrete Prototypes ( Circle and Rectangle ): These classes implement the clone() method, enabling them to be copied.
Prototype Factory ( PrototypeFactory ): The factory stores a collection of prototype objects. The createShape() method clones a prototype and returns the
copy to the client.
Main Function: Demonstrates how to create new objects using the prototype pattern by cloning existing shapes.
Key Points
Cloning an object is often faster than creating a new one from scratch.
The Prototype pattern allows for the flexibility to add or modify prototypes without changing the code that uses them.
You can enhance the Prototype pattern by using deep or shallow copies, depending on the requirements.
This pattern is particularly useful when dealing with complex objects that need to be instantiated multiple times with similar configurations.
6
Factory Design Pattern
Introduction
The Factory Design Pattern is a creational design pattern that provides a way to create objects without directly specifying their exact class. Instead, you use a
factory that knows how to create different types of objects. It’s useful when you have a common interface or base class and you want to delegate the responsibility of
instantiating concrete classes to a factory.
Key Concepts
Factory Method: A method in the factory class that decides which concrete class to instantiate based on input parameters.
Concrete Product: The specific implementation of the product that the factory creates.
Product Interface: A common interface or abstract class that all products share.
When to Use the Factory Pattern
When you want to centralize the creation of objects to ensure consistency and control.
When you have a common interface or base class, and the exact type of the object isn't known until runtime.
When the creation process involves complex logic that should not be exposed to the client.
Example in C++
Code Overview
Below is an example of a simple factory that creates different types of Shape objects like Circle , Rectangle , and Triangles .
#include <iostream>
// Interface and should only contain all the pure virtual functions.
class Shape {
public:
virtual void draw() const = 0;
virtual ~Shape() {}
};
// Factory Class. Note that this class can provide some default implementation of the factory method too. All functions need not be pure virtual
class ShapeFactory {
public:
enum ShapeType {
CIRCLE,
RECTANGLE,
TRIANGLE
};
7
return nullptr;
}
}
};
int main() {
ShapeFactory::ShapeType shapeType;
return 0;
}
Explanation
Product Interface: The Shape class defines the interface for all shape objects.
Concrete Products: Circle , Rectangle , and Triangle implement the Shape interface and provide specific behaviors for each shape.
Factory Class: The ShapeFactory class contains a method createShape that takes a ShapeType enum and returns a new instance of the corresponding
shape.
Client Code: The main function shows how to use the factory to create different shapes without knowing the details of how they are created.
Benefits of the Factory Pattern
Encapsulation: The object creation logic is centralized in the factory, making the code easier to manage and modify.
Flexibility: New product types can be added with minimal changes to the client code.
Decoupling: The client code is decoupled from the concrete classes it uses, depending only on the product interface.
Conclusion
The Factory Design Pattern simplifies object creation by centralizing it within a factory. This pattern is ideal for scenarios where the exact type of object to create isn't
known until runtime, or when the object creation involves complex logic.
8
Abstract Factory Pattern in C++
Overview
The Abstract Factory Pattern is a creational design pattern that provides an interface for creating families of related or dependent objects without specifying their
concrete classes. This pattern is useful when you need to ensure that a set of related objects is used together.
Key Points:
Abstract Factory Pattern: Creates families of related objects.
Factory Method Pattern: Creates a single object but defers instantiation to subclasses.
Why Use Abstract Factory?
When you have multiple related objects (like buttons, checkboxes, etc.) that belong to different families (e.g., Windows, Mac), and you want to ensure that you use
the objects from the same family together, the Abstract Factory Pattern is a good choice. This pattern helps to maintain consistency among products.
Example Scenario
Imagine you're designing a user interface (UI) toolkit that should work on different platforms like Windows and Mac. You want to create platform-specific UI elements
like buttons and checkboxes, but you want to ensure that a Windows button is used with a Windows checkbox, and similarly for Mac.
Components of Abstract Factory Pattern
1. Abstract Product: Declares an interface for a type of product object. (e.g., Button , Checkbox )
2. Concrete Product: Implements the abstract product interface. (e.g., WindowsButton , MacButton , WindowsCheckbox , MacCheckbox )
3. Abstract Factory: Declares an interface for creating abstract products. (e.g., GUIFactory )
4. Concrete Factory: Implements the operations to create concrete product objects. (e.g., WindowsFactory , MacFactory )
Implementation
Here's a simple C++ implementation of the Abstract Factory Pattern:
#include <iostream>
9
void render() const override {
std::cout << "Rendering Mac Checkbox." << std::endl;
}
};
// Abstract Factory
class GUIFactory {
public:
virtual Button* createButton() const = 0;
virtual Checkbox* createCheckbox() const = 0;
virtual ~GUIFactory() {}
};
int main() {
GUIFactory* factory;
button->render();
checkbox->render();
delete button;
delete checkbox;
delete factory; // Clean up to avoid memory leaks
button->render();
checkbox->render();
delete button;
delete checkbox;
delete factory; // Clean up to avoid memory leaks
return 0;
}
11
Builder Design Pattern
Overview
The Builder Design Pattern is a creational pattern used to construct complex objects step by step. Unlike other creational patterns that create objects in a single step,
the Builder Pattern allows you to build an object by specifying its various parts or properties one at a time. This approach is particularly useful when an object has
many attributes or when different variations of the object need to be created.
When to Use the Builder Pattern?
When the object to be created is complex and has multiple attributes.
When you want to construct different representations of the same object.
When you need to create an object step by step and ensure that all the necessary steps are performed.
Use the Builder pattern to get rid of a “telescoping constructor”.
Say you have a constructor with ten optional parameters. Calling such a beast is very inconvenient; therefore, you overload the constructor and create several shorter
versions with fewer parameters. These constructors still refer to the main one, passing some default values into any omitted parameters.
class Pizza {
Pizza(int size) { ... }
Pizza(int size, boolean cheese) { ... }
Pizza(int size, boolean cheese, boolean pepperoni) { ... }
//.....
};
The Builder pattern lets you build objects step by step, using only those steps that you need.
Structure
The Builder Pattern involves the following key components:
1. Builder (Interface): Declares the building steps that are common to all types of builders. Each step typically returns the builder itself, allowing for method
chaining.
2. Concrete Builder: Implements the building steps defined by the Builder interface. This is where the actual construction of the product happens.
3. Product: The complex object that is being built. The Builder sets its parts or attributes.
4. Director (Optional): An optional component that controls the construction process using a builder instance. It ensures that the construction steps are executed
in a particular order.
UML Diagram
Director ----> Builder (Interface)
^
|
ConcreteBuilder
|
|
Product
Example in C++
To fully understand the Builder Pattern, let's explore a more detailed example where we differentiate between basic and advanced features. We'll construct a Car
object that can have both essential features (like engine and seats) and optional advanced features (like GPS, sunroof, and a high-end sound system).
Problem: Complex Object Construction
Imagine you are designing a Car class where the car can have a variety of configurations. Some cars might just need the basics (engine, seats), while others might
need more luxurious options (GPS, sunroof). Using traditional constructors would lead to a telescopic constructor issue where you’d have constructors with an
increasingly large number of parameters, making the code hard to read and maintain.
Solution: Builder Pattern
With the Builder Pattern, you can construct a Car step by step, making the process clear and flexible. We’ll separate the construction of basic features and optional
advanced features to show how the pattern can simplify object creation.
Step 1: Define the Car Product
First, define the Car class with the attributes that need to be set.
12
#include <iostream>
#include <string>
class Car {
private:
std::string engine;
int seats;
bool hasGPS;
bool hasSunroof;
std::string soundSystem;
public:
void setEngine(const std::string& engine) { this->engine = engine; }
void setSeats(int seats) { this->seats = seats; }
void setGPS(bool hasGPS) { this->hasGPS = hasGPS; }
void setSunroof(bool hasSunroof) { this->hasSunroof = hasSunroof; }
void setSoundSystem(const std::string& soundSystem) { this->soundSystem = soundSystem; }
public:
LuxuryCarBuilder() { car = new Car(); }
// Optional Features
void buildGPS() override { car->setGPS(true); }
void buildSunroof() override { car->setSunroof(true); }
void buildSoundSystem() override { car->setSoundSystem("Bose Surround Sound"); }
public:
void setBuilder(CarBuilder* builder) { this->builder = builder; }
13
}
director.setBuilder(&builder);
return 0;
}
How It Works
Basic Features: The buildEngine and buildSeats methods are called to create the basic version of the Car .
Optional Features: The buildGPS , buildSunroof , and buildSoundSystem methods add advanced features to the car. These methods can be skipped if you
only need a basic version of the car.
Director: The CarDirector simplifies the client code by encapsulating the construction logic, ensuring that the car is built in a valid and consistent manner.
Benefits of the Builder Pattern
Handles Telescopic Constructors: The Builder Pattern avoids the problem of telescopic constructors by allowing you to add only the necessary parameters
during construction.
Clear Separation: There's a clear separation between the construction of basic and advanced features, making the code easier to understand and maintain.
Flexible Object Creation: The same builder can be used to create different configurations of the product, like a basic or luxury car.
Improves Readability: The step-by-step construction process is clear and easy to follow.
Flexible Object Creation: You can create different variations of the object using the same construction process.
Encapsulation: The construction details are hidden from the client, promoting encapsulation.
Conclusion
The Builder Pattern is an excellent solution when you need to construct complex objects with a mix of required and optional features. It keeps the code organized,
makes object construction flexible, and eliminates the issues caused by telescopic constructors.
14
Adapter Design Pattern in C++
Introduction
The Adapter Pattern is a structural design pattern that allows objects with incompatible interfaces to collaborate. It acts as a bridge between two incompatible
interfaces by wrapping an existing class with a new interface, making it compatible with another class.
Use Case
Imagine you have a class that expects a certain interface, but you have another class that provides the functionality you need but with a different interface. Instead of
modifying the existing classes (which might not be possible), you create an Adapter class that makes the two classes work together.
Structure
The Adapter Pattern typically involves the following components:
1. Target Interface: The interface that the client expects to work with.
2. Adaptee: The existing class that needs to be adapted to the Target Interface.
3. Adapter: A class that implements the Target Interface and translates the requests from the client to the Adaptee.
Example
Let's consider an example where you have an existing media player that only supports playing MP3 files, but you want to play MP4 and VLC files without changing the
existing media player code.
#include <iostream>
#include <string>
// Target Interface
class MediaPlayer {
public:
virtual void play(const std::string& audioType, const std::string& fileName) const = 0;
virtual ~MediaPlayer() {}
};
// Adaptee Interface
class AdvancedMediaPlayer {
public:
virtual void playMp4(const std::string& fileName) const = 0;
virtual void playVlc(const std::string& fileName) const = 0;
virtual ~AdvancedMediaPlayer() {}
};
15
// Adapter Class
class MediaAdapter : public MediaPlayer {
private:
AdvancedMediaPlayer* advancedPlayer;
public:
MediaAdapter(const std::string& audioType) {
if (audioType == "mp4") {
advancedPlayer = new Mp4Player();
} else if (audioType == "vlc") {
advancedPlayer = new VlcPlayer();
} else {
advancedPlayer = nullptr;
}
}
~MediaAdapter() {
delete advancedPlayer;
}
};
// Client: AudioPlayer
class AudioPlayer : public MediaPlayer {
private:
MediaAdapter* mediaAdapter;
Mp3Player* mp3Player;
public:
AudioPlayer() : mediaAdapter(nullptr), mp3Player(new Mp3Player()) {}
~AudioPlayer() {
delete mp3Player;
}
};
return 0;
}
16
Signs of Adapter Pattern in Code
The presence of a class that implements a known interface but internally delegates calls to a different class.
Code where existing classes or APIs are being adapted to a new interface without modifying the original classes.
The use of composition over inheritance, where the adapter holds a reference to the adaptee.
Conclusion
The Adapter Pattern is a powerful tool in your design pattern toolkit, allowing you to integrate and reuse classes with incompatible interfaces. By identifying the need
for an Adapter, you can avoid modifying existing classes and instead wrap them with a class that translates the interface, keeping your code flexible and maintainable.
17
Composite Design Pattern in C++
The Composite Pattern is a structural design pattern used to represent part-whole hierarchies. This pattern allows individual objects and groups of objects to be
treated uniformly.
Overview
In the Composite Pattern:
Component Interface: Defines a common interface for all objects in the composition.
Leaf: Represents a simple object in the hierarchy (e.g., a file).
Composite: Represents a container (e.g., a directory) that can hold other components.
Example: File System
We will use the Composite Pattern to implement a file system where:
A File is a leaf node.
A Directory is a composite node that can contain both files and other directories.
Implementation in C++
1. Define the Component Interface
The FileSystemEntity interface defines the common behavior for both File and Directory .
#include <iostream>
#include <vector>
#include <memory>
#include <string>
#include <algorithm>
class FileSystemEntity {
public:
virtual void display(int indent = 0) const = 0;
virtual ~FileSystemEntity() = default;
};
public:
explicit File(const std::string& fileName) : name(fileName) {}
public:
explicit Directory(const std::string& dirName) : name(dirName) {}
18
}
// Create directories
auto dir1 = std::make_shared<Directory>("Dir1");
auto dir2 = std::make_shared<Directory>("Dir2");
auto root = std::make_shared<Directory>("Root");
return 0;
}
5. Example Output
When the above code is executed, the following output is produced:
Directory: Root
Directory: Dir1
File: File1.txt
File: File2.txt
Directory: Dir2
File: File3.txt
Advantages
1. Uniformity: Treats individual and composite objects uniformly.
2. Scalability: Easily add new types of components without changing existing code.
3. Hierarchy Representation: Ideal for tree-like structures.
Disadvantages
1. Complexity: Recursive structures can make debugging and maintenance harder.
2. Overhead: Storing and traversing large trees may consume significant resources.
Use Cases
File systems (e.g., files and directories).
Organization charts (e.g., employees and managers).
GUI frameworks (e.g., windows and widgets).
Graphics systems (e.g., shapes composed of lines and circles).
Conclusion
The Composite Pattern simplifies working with hierarchical data structures by enabling the uniform treatment of individual and composite objects. It is widely used in
scenarios requiring tree-like structures.
19
Decorator Design Pattern in C++
The Decorator Pattern is a structural design pattern used to dynamically add new behavior to objects without altering their structure. This pattern adheres to the
Open/Closed Principle by allowing functionality to be extended without modifying existing code.
Overview
In the Decorator Pattern:
Component Interface: Defines the interface for objects that can have responsibilities added to them.
Concrete Component: The base implementation of the component interface.
Decorator: A class that wraps a component and adds additional behavior.
Concrete Decorators: Implement specific enhancements to the component.
Example: Notification System
We will use the Decorator Pattern to implement a notification system where:
A Notifier interface defines the common behavior.
A BasicNotifier sends basic notifications.
Decorators add additional notification methods (e.g., SMS, Email, Push notifications).
Implementation in C++
Here is the complete code for the Decorator Pattern:
#include <iostream>
#include <memory>
#include <string>
// Component Interface
class Notifier {
public:
virtual void send(const std::string& message) const = 0;
virtual ~Notifier() = default;
};
// Concrete Component
class BasicNotifier : public Notifier {
public:
void send(const std::string& message) const override {
std::cout << "Basic Notification: " << message << std::endl;
}
};
public:
explicit NotifierDecorator(std::shared_ptr<Notifier> notifier) : wrappedNotifier(std::move(notifier)) {}
void send(const std::string& message) const override {
wrappedNotifier->send(message);
}
};
// Concrete Decorators
class SMSNotifier : public NotifierDecorator {
public:
explicit SMSNotifier(std::shared_ptr<Notifier> notifier) : NotifierDecorator(std::move(notifier)) {}
20
public:
explicit PushNotifier(std::shared_ptr<Notifier> notifier) : NotifierDecorator(std::move(notifier)) {}
int main() {
// Create a basic notifier
auto basicNotifier = std::make_shared<BasicNotifier>();
// Send a notification
pushNotifier->send("Hello, World!");
return 0;
}
Example Output
When the above code is executed, the following output is produced:
Basic Notification: Hello, World!
SMS Notification: Hello, World!
Email Notification: Hello, World!
Push Notification: Hello, World!
Advantages
1. Flexibility: Add new behavior without modifying existing code.
2. Reusable Decorators: Decorators can be reused across multiple objects.
3. Open/Closed Principle: Extend functionality without altering base classes.
Disadvantages
1. Complexity: May introduce a large number of small classes.
2. Overhead: Wrapping objects can add overhead.
Use Cases
Adding cross-cutting concerns like logging, authentication, or caching.
Extending UI components in GUI frameworks.
Enhancing behaviors of objects in a modular way.
Conclusion
The Decorator Pattern provides a flexible and modular way to extend object behavior without altering the underlying class. It is particularly useful when dealing with
cross-cutting concerns or dynamically changing responsibilities.
21
Facade Design Pattern in C++
The Facade Pattern is a structural design pattern that provides a simplified interface to a larger body of code, such as a complex subsystem. It aims to make a system
easier to use by masking its complexity behind a single, unified interface.
Overview
In the Facade Pattern:
Facade: Provides a simple interface to the subsystem and delegates client requests to the appropriate subsystem classes.
Subsystems: Represent the complex system, which is often a set of interrelated classes.
Example: Home Theater System
We will use the Facade Pattern to control a home theater system with components such as a projector, sound system, and DVD player. The HomeTheaterFacade class
will provide a simplified interface to operate the entire system.
Implementation in C++
Here is the complete code for the Facade Pattern:
#include <iostream>
#include <string>
// Subsystem: Projector
class Projector {
public:
void on() const {
std::cout << "Projector is now ON." << std::endl;
}
void off() const {
std::cout << "Projector is now OFF." << std::endl;
}
void setWideScreenMode() const {
std::cout << "Projector is set to Widescreen Mode." << std::endl;
}
};
public:
void watchMovie(const std::string& movie) {
std::cout << "\nPreparing to watch a movie..." << std::endl;
projector.on();
projector.setWideScreenMode();
soundSystem.on();
soundSystem.setVolume(50);
22
dvdPlayer.on();
dvdPlayer.play(movie);
std::cout << "Enjoy your movie!\n" << std::endl;
}
void endMovie() {
std::cout << "\nShutting down the home theater system..." << std::endl;
dvdPlayer.off();
soundSystem.off();
projector.off();
std::cout << "Goodbye!\n" << std::endl;
}
};
int main() {
// Create the facade
HomeTheaterFacade homeTheater;
return 0;
}
Example Output
When the above code is executed, the following output is produced:
Preparing to watch a movie...
Projector is now ON.
Projector is set to Widescreen Mode.
Sound System is now ON.
Sound System volume set to 50.
DVD Player is now ON.
Playing movie: Inception
Enjoy your movie!
Advantages
1. Simplified Interface: Makes complex subsystems easier to use.
2. Decoupling: Shields clients from the details of the subsystem.
3. Flexibility: Allows subsystems to evolve independently of their clients.
Disadvantages
1. Limited Control: May restrict the advanced features of the subsystem.
2. Dependency: The facade class becomes a single point of modification.
Use Cases
Complex systems requiring a simple and consistent interface.
Libraries with intricate APIs that need to be hidden from the user.
Applications where subsystems may change without affecting the client.
Conclusion
The Facade Pattern provides a convenient interface to a complex system, improving usability and reducing dependencies. It is widely used in software libraries and
frameworks to simplify interactions with their components.
23
Flyweight Design Pattern in C++
The Flyweight Pattern is a structural design pattern that reduces memory usage by sharing common data among multiple objects. It separates intrinsic (shared) and
extrinsic (unique) data, allowing a large number of fine-grained objects to be managed efficiently.
Overview
In the Flyweight Pattern:
Flyweight Interface: Defines a common interface for shared objects.
Concrete Flyweight: Implements the Flyweight interface and shares data.
Flyweight Factory: Manages and provides shared Flyweight objects.
Client: Combines shared Flyweight objects with unique extrinsic states.
Example: Text Formatting
We will use the Flyweight Pattern to render text with shared character formatting (intrinsic state) and unique positioning (extrinsic state).
Implementation in C++
Here is the complete code for the Flyweight Pattern:
#include <iostream>
#include <unordered_map>
#include <memory>
#include <string>
// Flyweight Interface
class Character {
public:
virtual void display(int positionX, int positionY) const = 0;
virtual ~Character() = default;
};
public:
ConcreteCharacter(char sym, const std::string& f) : symbol(sym), font(f) {}
// Flyweight Factory
class CharacterFactory {
private:
std::unordered_map<char, std::shared_ptr<Character>> characters;
public:
std::shared_ptr<Character> getCharacter(char symbol, const std::string& font) {
auto it = characters.find(symbol);
if (it == characters.end()) {
auto character = std::make_shared<ConcreteCharacter>(symbol, font);
characters[symbol] = character;
return character;
}
return it->second;
}
};
// Client Code
int main() {
CharacterFactory factory;
// Shared Characters
auto charA = factory.getCharacter('A', "Arial");
auto charB = factory.getCharacter('B', "Arial");
auto charARepeat = factory.getCharacter('A', "Arial"); // Reuses the existing 'A'
24
return 0;
}
Example Output
When the above code is executed, the following output is produced:
Character: A, Font: Arial, Position: (10, 20)
Character: B, Font: Arial, Position: (15, 25)
Character: A, Font: Arial, Position: (30, 40)
Advantages
1. Reduced Memory Usage: Minimizes duplication of intrinsic state.
2. Efficient Object Management: Ideal for systems with many similar objects.
Disadvantages
1. Complexity: Increases code complexity due to state separation.
2. Limited Flexibility: Requires careful design to separate intrinsic and extrinsic data.
Use Cases
Text editors (e.g., shared glyph data for characters).
Graphics systems (e.g., shared object models in 3D rendering).
Game development (e.g., managing many similar entities like trees or enemies).
Conclusion
The Flyweight Pattern is a powerful tool for optimizing memory usage in applications with many similar objects. By sharing intrinsic data and managing unique
extrinsic state, it strikes a balance between efficiency and functionality.
25
Proxy Design Pattern in C++
The Proxy Pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. It allows adding additional behavior
or managing access to the real object without modifying it directly.
Overview
In the Proxy Pattern:
Subject Interface: Defines the common interface for RealSubject and Proxy.
RealSubject: The actual object that performs the core operations.
Proxy: Acts as a placeholder for the RealSubject, adding functionality like access control, logging, or lazy initialization.
Example: Image Viewer
We will implement an image viewer where:
A RealImage is the actual image being displayed.
A ProxyImage acts as a proxy for RealImage , loading it lazily and adding logging functionality.
Implementation in C++
Here is the complete code for the Proxy Pattern:
#include <iostream>
#include <string>
#include <memory>
// Subject Interface
class Image {
public:
virtual void display() const = 0;
virtual ~Image() = default;
};
// RealSubject
class RealImage : public Image {
private:
std::string fileName;
public:
explicit RealImage(const std::string& file) : fileName(file) {
loadFromDisk();
}
// Proxy
class ProxyImage : public Image {
private:
std::string fileName;
mutable std::shared_ptr<RealImage> realImage; // Lazy initialization
public:
explicit ProxyImage(const std::string& file) : fileName(file), realImage(nullptr) {}
int main() {
// Use ProxyImage to access RealImage
ProxyImage proxyImage("test_image.jpg");
26
proxyImage.display();
return 0;
}
Example Output
When the above code is executed, the following output is produced:
First call to display:
Loading image from disk: test_image.jpg
Displaying image: test_image.jpg
Advantages
1. Lazy Initialization: The real object is created only when needed, saving resources.
2. Access Control: Restrict or control access to the real object.
3. Additional Behavior: Add logging, caching, or other functionality transparently.
Disadvantages
1. Complexity: Adds an extra layer of indirection, which can make debugging harder.
2. Performance Overhead: Proxy adds overhead, especially if the real object is lightweight.
Use Cases
Virtual Proxy: Delay the creation and initialization of expensive objects.
Remote Proxy: Represent an object in a different address space (e.g., network communication).
Protection Proxy: Control access to sensitive objects based on permissions.
Logging Proxy: Add logging or tracking functionality.
Conclusion
The Proxy Pattern provides a flexible way to manage access to an object while keeping its interface consistent. It is particularly useful for scenarios requiring lazy
initialization, logging, or access control.
27
Chain of Responsibility Design Pattern in C++
The Chain of Responsibility Pattern is a behavioral design pattern that allows a request to be passed along a chain of handlers until one of them handles it. This
pattern decouples the sender of the request from its receivers.
Overview
In the Chain of Responsibility Pattern:
Handler Interface: Defines the interface for handling requests and setting the next handler.
Concrete Handlers: Implement the handler interface and decide whether to process a request or pass it to the next handler.
Client: Initiates requests to be handled by the chain.
Example: Customer Support System
We will implement a customer support system where requests are passed along a chain of support levels (e.g., Low-Level Support, Mid-Level Support, and High-
Level Support).
Implementation in C++
Here is the complete code for the Chain of Responsibility Pattern:
#include <iostream>
#include <memory>
#include <string>
// Handler Interface
class SupportHandler {
protected:
std::shared_ptr<SupportHandler> nextHandler;
public:
void setNextHandler(const std::shared_ptr<SupportHandler>& handler) {
nextHandler = handler;
}
28
// Client Code
int main() {
// Create support handlers
auto lowLevel = std::make_shared<LowLevelSupport>();
auto midLevel = std::make_shared<MidLevelSupport>();
auto highLevel = std::make_shared<HighLevelSupport>();
return 0;
}
Example Output
When the above code is executed, the following output is produced:
Low-Level Support: Handling issue - Password reset
Mid-Level Support: Handling issue - System crash
High-Level Support: Handling issue - Data breach
High-Level Support: Unable to handle issue. Escalating further.
Advantages
1. Decoupling: Decouples the sender of a request from its receivers.
2. Flexibility: Easily add or reorder handlers in the chain.
3. Responsibility Sharing: Distributes the handling of requests among multiple objects.
Disadvantages
1. Uncertainty: No guarantee that a request will be handled.
2. Debugging: Can make debugging harder due to dynamic behavior.
Use Cases
Logging frameworks with different levels (e.g., debug, info, error).
Customer support systems with multiple support levels.
Event handling in GUI frameworks.
Conclusion
The Chain of Responsibility Pattern provides a flexible and scalable approach to handle requests by passing them along a chain of handlers. It is particularly useful in
systems requiring dynamic and decoupled request handling.
29
Iterator Design Pattern in C++
The Iterator Pattern is a behavioral design pattern that provides a way to sequentially access elements of a collection without exposing its underlying representation.
Overview
In the Iterator Pattern:
Iterator Interface: Defines methods to traverse through elements.
Concrete Iterator: Implements the iterator interface for a specific collection.
Aggregate Interface: Defines a method to create an iterator.
Concrete Aggregate: Implements the aggregate interface and holds the collection.
Client: Uses the iterator to access elements.
Example: Book Collection
We will implement a book collection where an iterator is used to traverse through the list of books.
Implementation in C++
Here is the complete code for the Iterator Pattern:
#include <iostream>
#include <vector>
#include <memory>
#include <string>
// Iterator Interface
template <typename T>
class Iterator {
public:
virtual bool hasNext() const = 0;
virtual T next() = 0;
virtual ~Iterator() = default;
};
// Concrete Iterator
class BookIterator : public Iterator<std::string> {
private:
const std::vector<std::string>& books;
size_t index;
public:
explicit BookIterator(const std::vector<std::string>& bookCollection)
: books(bookCollection), index(0) {}
// Aggregate Interface
class BookCollection {
public:
virtual std::shared_ptr<Iterator<std::string>> createIterator() const = 0;
virtual ~BookCollection() = default;
};
// Concrete Aggregate
class Library : public BookCollection {
private:
std::vector<std::string> books;
public:
void addBook(const std::string& book) {
books.push_back(book);
}
30
// Client Code
int main() {
// Create a library and add books
Library library;
library.addBook("The Catcher in the Rye");
library.addBook("To Kill a Mockingbird");
library.addBook("1984");
library.addBook("Moby-Dick");
return 0;
}
Example Output
When the above code is executed, the following output is produced:
Books in the library:
- The Catcher in the Rye
- To Kill a Mockingbird
- 1984
- Moby-Dick
Advantages
1. Encapsulation: The underlying structure of the collection is hidden from the client.
2. Flexibility: Iterators can work with different types of collections.
3. Reusability: Iterator logic is reusable across various client applications.
Disadvantages
1. Overhead: Creating and managing iterator objects may add complexity.
2. Single Responsibility: Adding an iterator might violate the Single Responsibility Principle for simple collections.
Use Cases
Iterating over a collection of objects.
Implementing traversal algorithms in tree or graph structures.
Standard Template Library (STL) containers in C++.
Conclusion
The Iterator Pattern provides a robust mechanism for accessing elements in a collection while abstracting away the underlying details of the collection's structure. It
is widely used in scenarios requiring sequential traversal.
31
Practice Questions to Test Your Design
Skills
Welcome to the part where you implement what you learned!
These fun and challenging scenarios are crafted to help you flex your design muscles. By solving
these real-world problems, you'll get hands-on experience with design patterns and strengthen
your problem-solving abilities.
How to Get Started
1. Read the scenario and understand the requirements.
2. Think about the best design patterns to address the challenges.
3. Fire up your editor and start coding in your favorite language (C++ is a great choice!).
4. Verify your solution and see if it ticks all the boxes.
A Quick Note
You don’t need to build the entire system for these scenarios. Instead, focus on creating a
thoughtful class design that outlines the structure and relationships between key components. This
will help you practice designing robust and scalable architectures.
Ready? Let’s dive in!
Important Tip
For each scenario:
1. Analyze the problem and break it down into clear requirements.
2. Identify the design pattern(s) that best address the challenges.
3. Explain your reasoning for choosing the pattern(s) with real-world relevance.
4. Implement your solution step-by-step, ensuring it meets the requirements.
5. Test your design to confirm it works as intended.
Enjoy the process, and remember—great design takes practice. You’ve got this!