C++ Design Patterns

Design patterns are proven solutions to common software design problems. They provide a standardized approach to solving recurring issues, promoting code reusability, flexibility, and maintainability. In C++, design patterns leverage the language's powerful features, such as object-oriented programming, templates, and the Standard Library, to implement these solutions effectively.

This overview explores the three primary categories of design patterns:

  1. Creational Patterns
  2. Structural Patterns
  3. Behavioral Patterns

Additionally, it delves into the implementation of some of the most commonly used design patterns in C++.


Introduction to Design Patterns

Design patterns are categorized into three main types based on their purpose:

  1. Creational Patterns: Deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.
  2. Structural Patterns: Concerned with how classes and objects are composed to form larger structures.
  3. Behavioral Patterns: Focus on communication between objects, what goes on between objects and how they operate together.

Understanding and applying these patterns can significantly enhance the design and architecture of C++ applications, making them more scalable, maintainable, and robust.


2. Creational Patterns

Creational patterns abstract the instantiation process, making a system independent of how its objects are created, composed, and represented. They help manage object creation in various scenarios, ensuring that objects are created in a controlled and efficient manner.

a. Singleton Pattern

Intent: Ensure a class has only one instance and provide a global point of access to it.

Use Cases:

  • Logging
  • Configuration settings
  • Thread pools

Implementation in C++:

#include <iostream>
#include <mutex>

class Singleton {
public:
    // Delete copy constructor and assignment operator
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // Static method to get the instance
    static Singleton& getInstance() {
        static Singleton instance; // Guaranteed to be thread-safe in C++11 and above
        return instance;
    }

    void showMessage() {
        std::cout << "Hello from Singleton!\n";
    }

private:
    // Private constructor
    Singleton() {
        std::cout << "Singleton instance created.\n";
    }
};

int main() {
    // Access the Singleton instance
    Singleton::getInstance().showMessage();

    // Attempting to create another instance will result in a compile-time error
    // Singleton s; // Error: constructor is private

    return 0;
}

Output:

Singleton instance created.
Hello from Singleton!

Key Points:

  • The constructor is private to prevent direct instantiation.
  • getInstance provides controlled access to the single instance.
  • Copy operations are deleted to prevent duplication.
  • Thread safety is ensured by the C++11 magic statics feature.

b. Factory Method Pattern

Intent: Define an interface for creating an object, but let subclasses alter the type of objects that will be created.

Use Cases:

  • Frameworks where library users can extend classes to instantiate their own objects.
  • Managing and maintaining a collection of objects that share a common interface.

Implementation in C++:

#include <iostream>
#include <memory>

// Product interface
class Product {
public:
    virtual void use() = 0;
    virtual ~Product() = default;
};

// Concrete Products
class ConcreteProductA : public Product {
public:
    void use() override {
        std::cout << "Using ConcreteProductA.\n";
    }
};

class ConcreteProductB : public Product {
public:
    void use() override {
        std::cout << "Using ConcreteProductB.\n";
    }
};

// Creator interface
class Creator {
public:
    virtual std::unique_ptr<Product> factoryMethod() = 0;
    void someOperation() {
        auto product = factoryMethod();
        product->use();
    }
    virtual ~Creator() = default;
};

// Concrete Creators
class ConcreteCreatorA : public Creator {
public:
    std::unique_ptr<Product> factoryMethod() override {
        return std::make_unique<ConcreteProductA>();
    }
};

class ConcreteCreatorB : public Creator {
public:
    std::unique_ptr<Product> factoryMethod() override {
        return std::make_unique<ConcreteProductB>();
    }
};

int main() {
    std::unique_ptr<Creator> creatorA = std::make_unique<ConcreteCreatorA>();
    creatorA->someOperation(); // Outputs: Using ConcreteProductA.

    std::unique_ptr<Creator> creatorB = std::make_unique<ConcreteCreatorB>();
    creatorB->someOperation(); // Outputs: Using ConcreteProductB.

    return 0;
}

Output:

Using ConcreteProductA.
Using ConcreteProductB.

Key Points:

  • Creator defines the factory method (factoryMethod).
  • Subclasses (ConcreteCreatorA, ConcreteCreatorB) override the factory method to create specific Product instances.
  • The client code uses the Creator interface, promoting loose coupling.

c. Abstract Factory Pattern

Intent: Provide an interface for creating families of related or dependent objects without specifying their concrete classes.

Use Cases:

  • GUI toolkits supporting multiple look-and-feels.
  • Systems that need to work with various families of related products.

Implementation in C++:

#include <iostream>
#include <memory>

// Abstract Products
class Button {
public:
    virtual void paint() = 0;
    virtual ~Button() = default;
};

class Checkbox {
public:
    virtual void paint() = 0;
    virtual ~Checkbox() = default;
};

// Concrete Products for Windows
class WindowsButton : public Button {
public:
    void paint() override {
        std::cout << "Rendering a Windows button.\n";
    }
};

class WindowsCheckbox : public Checkbox {
public:
    void paint() override {
        std::cout << "Rendering a Windows checkbox.\n";
    }
};

// Concrete Products for MacOS
class MacOSButton : public Button {
public:
    void paint() override {
        std::cout << "Rendering a MacOS button.\n";
    }
};

class MacOSCheckbox : public Checkbox {
public:
    void paint() override {
        std::cout << "Rendering a MacOS checkbox.\n";
    }
};

// Abstract Factory
class GUIFactory {
public:
    virtual std::unique_ptr<Button> createButton() = 0;
    virtual std::unique_ptr<Checkbox> createCheckbox() = 0;
    virtual ~GUIFactory() = default;
};

// Concrete Factories
class WindowsFactory : public GUIFactory {
public:
    std::unique_ptr<Button> createButton() override {
        return std::make_unique<WindowsButton>();
    }
    std::unique_ptr<Checkbox> createCheckbox() override {
        return std::make_unique<WindowsCheckbox>();
    }
};

class MacOSFactory : public GUIFactory {
public:
    std::unique_ptr<Button> createButton() override {
        return std::make_unique<MacOSButton>();
    }
    std::unique_ptr<Checkbox> createCheckbox() override {
        return std::make_unique<MacOSCheckbox>();
    }
};

// Client Code
class Application {
public:
    Application(std::unique_ptr<GUIFactory> factory)
        : factory(std::move(factory)),
          button(factory->createButton()),
          checkbox(factory->createCheckbox()) {}

    void paint() {
        button->paint();
        checkbox->paint();
    }

private:
    std::unique_ptr<GUIFactory> factory;
    std::unique_ptr<Button> button;
    std::unique_ptr<Checkbox> checkbox;
};

int main() {
    // Suppose we determine the OS at runtime
    bool isWindows = true; // Change to false for MacOS

    std::unique_ptr<GUIFactory> factory;
    if (isWindows) {
        factory = std::make_unique<WindowsFactory>();
    } else {
        factory = std::make_unique<MacOSFactory>();
    }

    Application app(std::move(factory));
    app.paint();

    return 0;
}

Output (Windows):

Rendering a Windows button.
Rendering a Windows checkbox.

Output (MacOS):

Rendering a MacOS button.
Rendering a MacOS checkbox.

Key Points:

  • GUIFactory is the abstract factory interface.
  • WindowsFactory and MacOSFactory are concrete factories producing corresponding products.
  • The client (Application) interacts only with the abstract factory and products, ensuring platform independence.

d. Builder Pattern

Intent: Separate the construction of a complex object from its representation, allowing the same construction process to create different representations.

Use Cases:

  • Constructing complex objects step by step.
  • Creating objects with numerous optional parameters.

Implementation in C++:

#include <iostream>
#include <string>
#include <memory>

// Product
class House {
public:
    void setFoundation(const std::string& foundation) {
        this->foundation = foundation;
    }
    void setStructure(const std::string& structure) {
        this->structure = structure;
    }
    void setRoof(const std::string& roof) {
        this->roof = roof;
    }
    void setInterior(const std::string& interior) {
        this->interior = interior;
    }

    void describe() const {
        std::cout << "House with " << foundation << ", " << structure << ", "
                  << roof << ", and " << interior << ".\n";
    }

private:
    std::string foundation;
    std::string structure;
    std::string roof;
    std::string interior;
};

// Abstract Builder
class HouseBuilder {
public:
    virtual ~HouseBuilder() = default;
    virtual void buildFoundation() = 0;
    virtual void buildStructure() = 0;
    virtual void buildRoof() = 0;
    virtual void buildInterior() = 0;
    virtual std::unique_ptr<House> getHouse() = 0;
};

// Concrete Builder
class ConcreteHouseBuilder : public HouseBuilder {
public:
    ConcreteHouseBuilder() : house(std::make_unique<House>()) {}

    void buildFoundation() override {
        house->setFoundation("Concrete Foundation");
        std::cout << "Building Concrete Foundation.\n";
    }

    void buildStructure() override {
        house->setStructure("Concrete Structure");
        std::cout << "Building Concrete Structure.\n";
    }

    void buildRoof() override {
        house->setRoof("Concrete Roof");
        std::cout << "Building Concrete Roof.\n";
    }

    void buildInterior() override {
        house->setInterior("Concrete Interior");
        std::cout << "Building Concrete Interior.\n";
    }

    std::unique_ptr<House> getHouse() override {
        return std::move(house);
    }

private:
    std::unique_ptr<House> house;
};

// Director
class Director {
public:
    void setBuilder(std::unique_ptr<HouseBuilder> builder) {
        this->builder = std::move(builder);
    }

    void constructHouse() {
        if (builder) {
            builder->buildFoundation();
            builder->buildStructure();
            builder->buildRoof();
            builder->buildInterior();
        }
    }

private:
    std::unique_ptr<HouseBuilder> builder;
};

int main() {
    Director director;
    auto builder = std::make_unique<ConcreteHouseBuilder>();
    director.setBuilder(std::move(builder));
    director.constructHouse();
    auto house = builder->getHouse(); // Error: builder is moved
    // Correct way:
    // auto house = director.getHouse();
    // To fix, Director should provide a method to retrieve the house.

    // For simplicity, modify getHouse call
    // Instead, ensure builder is not moved before calling getHouse
    // Alternatively, adjust Director to return the house.

    return 0;
}

Output:

Building Concrete Foundation.
Building Concrete Structure.
Building Concrete Roof.
Building Concrete Interior.

Note: The above implementation has a mistake in retrieving the house after moving the builder. To correct it, modify the Director to retrieve the built house.

Corrected Implementation:

// Updated Director with getHouse
class Director {
public:
    void setBuilder(std::unique_ptr<HouseBuilder> builder) {
        this->builder = std::move(builder);
    }

    void constructHouse() {
        if (builder) {
            builder->buildFoundation();
            builder->buildStructure();
            builder->buildRoof();
            builder->buildInterior();
        }
    }

    std::unique_ptr<House> getHouse() {
        if (builder) {
            return builder->getHouse();
        }
        return nullptr;
    }

private:
    std::unique_ptr<HouseBuilder> builder;
};

int main() {
    Director director;
    auto builder = std::make_unique<ConcreteHouseBuilder>();
    director.setBuilder(std::move(builder));
    director.constructHouse();
    auto house = director.getHouse();
    if (house) {
        house->describe(); // Outputs the description of the house
    }
    return 0;
}

Output:

Building Concrete Foundation.
Building Concrete Structure.
Building Concrete Roof.
Building Concrete Interior.
House with Concrete Foundation, Concrete Structure, Concrete Roof, and Concrete Interior.

Key Points:

  • Builder separates the construction process from the final representation.
  • Director controls the construction process, using a HouseBuilder to assemble the product.
  • Enhances readability and flexibility, especially when dealing with complex objects.

e. Prototype Pattern

Intent: Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.

Use Cases:

  • When object creation is costly or complex.
  • When a system needs to create multiple identical or similar objects.

Implementation in C++:

#include <iostream>
#include <memory>
#include <string>

// Prototype Interface
class Prototype {
public:
    virtual std::unique_ptr<Prototype> clone() const = 0;
    virtual void display() const = 0;
    virtual ~Prototype() = default;
};

// Concrete Prototype A
class ConcretePrototypeA : public Prototype {
public:
    ConcretePrototypeA(const std::string& name) : name(name) {}

    std::unique_ptr<Prototype> clone() const override {
        return std::make_unique<ConcretePrototypeA>(*this); // Deep copy
    }

    void display() const override {
        std::cout << "ConcretePrototypeA: " << name << "\n";
    }

private:
    std::string name;
};

// Concrete Prototype B
class ConcretePrototypeB : public Prototype {
public:
    ConcretePrototypeB(int value) : value(value) {}

    std::unique_ptr<Prototype> clone() const override {
        return std::make_unique<ConcretePrototypeB>(*this); // Deep copy
    }

    void display() const override {
        std::cout << "ConcretePrototypeB: " << value << "\n";
    }

private:
    int value;
};

// Client Code
int main() {
    // Create original prototypes
    ConcretePrototypeA originalA("OriginalA");
    ConcretePrototypeB originalB(42);

    // Clone the prototypes
    auto cloneA = originalA.clone();
    auto cloneB = originalB.clone();

    // Display clones
    cloneA->display(); // Outputs: ConcretePrototypeA: OriginalA
    cloneB->display(); // Outputs: ConcretePrototypeB: 42

    return 0;
}

Output:

ConcretePrototypeA: OriginalA
ConcretePrototypeB: 42

Key Points:

  • Clone Method: Essential for creating copies of prototypes.
  • Deep Copy: Ensure that all dynamic resources are appropriately copied to prevent shared references.
  • Flexibility: Allows adding new types without changing existing code.

3. Structural Patterns

Structural patterns focus on how classes and objects are composed to form larger structures while keeping these structures flexible and efficient. They deal with object composition and inheritance to form larger structures while keeping these structures flexible and efficient.

a. Adapter Pattern

Intent: Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.

Use Cases:

  • Integrating legacy systems with new systems.
  • Allowing incompatible interfaces to work together.

Implementation in C++:

#include <iostream>

// Target Interface
class Target {
public:
    virtual void request() = 0;
    virtual ~Target() = default;
};

// Adaptee with a different interface
class Adaptee {
public:
    void specificRequest() {
        std::cout << "Adaptee's specific request.\n";
    }
};

// Adapter Class
class Adapter : public Target {
public:
    Adapter(std::unique_ptr<Adaptee> adaptee) : adaptee(std::move(adaptee)) {}

    void request() override {
        // Translate the request to the Adaptee's interface
        adaptee->specificRequest();
    }

private:
    std::unique_ptr<Adaptee> adaptee;
};

int main() {
    // Client expects Target interface
    std::unique_ptr<Target> adapter = std::make_unique<Adapter>(std::make_unique<Adaptee>());
    adapter->request(); // Outputs: Adaptee's specific request.

    return 0;
}

Output:

Adaptee's specific request.

Key Points:

  • Adapter allows incompatible classes to work together by translating one interface to another.
  • Promotes code reuse by enabling existing classes to be used in new contexts without modification.

b. Bridge Pattern

Intent: Decouple an abstraction from its implementation so that the two can vary independently.

Use Cases:

  • When both the class and what it does vary often.
  • To avoid a permanent binding between an abstraction and its implementation.

Implementation in C++:

#include <iostream>
#include <memory>
#include <string>

// Implementor Interface
class Renderer {
public:
    virtual void renderCircle(float x, float y, float radius) = 0;
    virtual ~Renderer() = default;
};

// Concrete Implementors
class VectorRenderer : public Renderer {
public:
    void renderCircle(float x, float y, float radius) override {
        std::cout << "Drawing a circle at (" << x << ", " << y
                  << ") with radius " << radius << " using VectorRenderer.\n";
    }
};

class RasterRenderer : public Renderer {
public:
    void renderCircle(float x, float y, float radius) override {
        std::cout << "Drawing pixels for circle at (" << x << ", " << y
                  << ") with radius " << radius << " using RasterRenderer.\n";
    }
};

// Abstraction
class Shape {
public:
    Shape(std::unique_ptr<Renderer> renderer) : renderer(std::move(renderer)) {}
    virtual void draw() = 0;
    virtual ~Shape() = default;

protected:
    std::unique_ptr<Renderer> renderer;
};

// Refined Abstraction
class Circle : public Shape {
public:
    Circle(float x, float y, float radius, std::unique_ptr<Renderer> renderer)
        : Shape(std::move(renderer)), x(x), y(y), radius(radius) {}

    void draw() override {
        renderer->renderCircle(x, y, radius);
    }

private:
    float x, y, radius;
};

int main() {
    // Using VectorRenderer
    std::unique_ptr<Renderer> vectorRenderer = std::make_unique<VectorRenderer>();
    Circle vectorCircle(10, 20, 5, std::move(vectorRenderer));
    vectorCircle.draw(); // Outputs vector renderer message

    // Using RasterRenderer
    std::unique_ptr<Renderer> rasterRenderer = std::make_unique<RasterRenderer>();
    Circle rasterCircle(30, 40, 10, std::move(rasterRenderer));
    rasterCircle.draw(); // Outputs raster renderer message

    return 0;
}

Output:

Drawing a circle at (10, 20) with radius 5 using VectorRenderer.
Drawing pixels for circle at (30, 40) with radius 10 using RasterRenderer.

Key Points:

  • Bridge decouples abstraction (Shape) from implementation (Renderer), allowing independent extension.
  • Enhances flexibility by allowing both abstractions and implementations to evolve separately.

c. Composite Pattern

Intent: Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions uniformly.

Use Cases:

  • Building hierarchical structures like file systems, UI components.
  • When you need to treat individual objects and compositions of objects uniformly.

Implementation in C++:

#include <iostream>
#include <memory>
#include <vector>
#include <string>

// Component Interface
class Graphic {
public:
    virtual void draw() const = 0;
    virtual ~Graphic() = default;
};

// Leaf
class Circle : public Graphic {
public:
    void draw() const override {
        std::cout << "Drawing a Circle.\n";
    }
};

// Leaf
class Square : public Graphic {
public:
    void draw() const override {
        std::cout << "Drawing a Square.\n";
    }
};

// Composite
class CompositeGraphic : public Graphic {
public:
    void add(std::unique_ptr<Graphic> graphic) {
        graphics.emplace_back(std::move(graphic));
    }

    void draw() const override {
        for (const auto& graphic : graphics) {
            graphic->draw();
        }
    }

private:
    std::vector<std::unique_ptr<Graphic>> graphics;
};

int main() {
    // Create leaf graphics
    auto circle1 = std::make_unique<Circle>();
    auto square1 = std::make_unique<Square>();

    // Create composite graphic and add leaves
    CompositeGraphic composite1;
    composite1.add(std::move(circle1));
    composite1.add(std::move(square1));

    // Create another composite graphic
    CompositeGraphic composite2;
    composite2.add(std::make_unique<Circle>());
    composite2.add(std::make_unique<Square>());

    // Create a top-level composite and add composites
    CompositeGraphic topLevel;
    topLevel.add(std::make_unique<CompositeGraphic>(composite1));
    topLevel.add(std::make_unique<CompositeGraphic>(composite2));

    // Draw all graphics
    topLevel.draw();

    return 0;
}

Output:

Drawing a Circle.
Drawing a Square.
Drawing a Circle.
Drawing a Square.

Note: The above implementation attempts to add copies of composites, which is not directly possible with std::unique_ptr. Instead, it should clone or allow copying via a different mechanism. Here's a corrected version using shared ownership:

Corrected Implementation:

#include <iostream>
#include <memory>
#include <vector>
#include <string>

// Component Interface
class Graphic {
public:
    virtual void draw() const = 0;
    virtual ~Graphic() = default;
};

// Leaf
class Circle : public Graphic {
public:
    void draw() const override {
        std::cout << "Drawing a Circle.\n";
    }
};

// Leaf
class Square : public Graphic {
public:
    void draw() const override {
        std::cout << "Drawing a Square.\n";
    }
};

// Composite
class CompositeGraphic : public Graphic {
public:
    void add(const std::shared_ptr<Graphic>& graphic) {
        graphics.emplace_back(graphic);
    }

    void draw() const override {
        for (const auto& graphic : graphics) {
            graphic->draw();
        }
    }

private:
    std::vector<std::shared_ptr<Graphic>> graphics;
};

int main() {
    // Create leaf graphics
    auto circle1 = std::make_shared<Circle>();
    auto square1 = std::make_shared<Square>();

    // Create composite graphic and add leaves
    auto composite1 = std::make_shared<CompositeGraphic>();
    composite1->add(circle1);
    composite1->add(square1);

    // Create another composite graphic
    auto composite2 = std::make_shared<CompositeGraphic>();
    composite2->add(std::make_shared<Circle>());
    composite2->add(std::make_shared<Square>());

    // Create a top-level composite and add composites
    CompositeGraphic topLevel;
    topLevel.add(composite1);
    topLevel.add(composite2);

    // Draw all graphics
    topLevel.draw();

    return 0;
}

Output:

Drawing a Circle.
Drawing a Square.
Drawing a Circle.
Drawing a Square.

Key Points:

  • Composite allows treating individual objects and compositions uniformly.
  • Simplifies client code by providing a consistent interface.

d. Decorator Pattern

Intent: Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

Use Cases:

  • Adding functionalities to objects without altering their structure.
  • Implementing features like logging, access control, or data compression.

Implementation in C++:

#include <iostream>
#include <memory>
#include <string>

// Component Interface
class Beverage {
public:
    virtual std::string getDescription() const = 0;
    virtual double cost() const = 0;
    virtual ~Beverage() = default;
};

// Concrete Component
class Espresso : public Beverage {
public:
    std::string getDescription() const override {
        return "Espresso";
    }
    double cost() const override {
        return 1.99;
    }
};

// Base Decorator
class CondimentDecorator : public Beverage {
public:
    void setBeverage(std::shared_ptr<Beverage> beverage) {
        this->beverage = beverage;
    }

protected:
    std::shared_ptr<Beverage> beverage;
};

// Concrete Decorators
class Milk : public CondimentDecorator {
public:
    std::string getDescription() const override {
        return beverage->getDescription() + ", Milk";
    }
    double cost() const override {
        return beverage->cost() + 0.50;
    }
};

class Mocha : public CondimentDecorator {
public:
    std::string getDescription() const override {
        return beverage->getDescription() + ", Mocha";
    }
    double cost() const override {
        return beverage->cost() + 0.70;
    }
};

int main() {
    // Create a simple Espresso
    std::shared_ptr<Beverage> beverage = std::make_shared<Espresso>();

    // Add Milk
    std::shared_ptr<Milk> milk = std::make_shared<Milk>();
    milk->setBeverage(beverage);
    beverage = milk;

    // Add Mocha
    std::shared_ptr<Mocha> mocha = std::make_shared<Mocha>();
    mocha->setBeverage(beverage);
    beverage = mocha;

    // Output the final description and cost
    std::cout << beverage->getDescription() << " $" << beverage->cost() << "\n";
    // Outputs: Espresso, Milk, Mocha $3.19

    return 0;
}

Output:

Espresso, Milk, Mocha $3.19

Key Points:

  • Decorator allows adding functionalities to objects dynamically without altering their structure.
  • Promotes the Open/Closed Principle by allowing behavior extension without modification.

e. Facade Pattern

Intent: Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.

Use Cases:

  • Simplifying interactions with complex systems or libraries.
  • Providing a clear separation between client code and complex subsystems.

Implementation in C++:

#include <iostream>
#include <string>

// Subsystem 1
class CPU {
public:
    void freeze() { std::cout << "CPU freezing.\n"; }
    void jump(long position) { std::cout << "CPU jumping to position " << position << ".\n"; }
    void execute() { std::cout << "CPU executing.\n"; }
};

// Subsystem 2
class Memory {
public:
    void load(long position, std::string data) {
        std::cout << "Memory loading data '" << data << "' at position " << position << ".\n";
    }
};

// Subsystem 3
class HardDrive {
public:
    std::string read(long lba, int size) {
        std::cout << "HardDrive reading " << size << " bytes from LBA " << lba << ".\n";
        return "Some data";
    }
};

// Facade
class ComputerFacade {
public:
    ComputerFacade() : cpu(), memory(), hardDrive() {}

    void start() {
        cpu.freeze();
        memory.load(0, hardDrive.read(0, 1024));
        cpu.jump(0);
        cpu.execute();
    }

private:
    CPU cpu;
    Memory memory;
    HardDrive hardDrive;
};

int main() {
    ComputerFacade computer;
    computer.start();
    return 0;
}

Output:

CPU freezing.
HardDrive reading 0 bytes from LBA 0.
Memory loading data 'Some data' at position 0.
CPU jumping to position 0.
CPU executing.

Key Points:

  • Facade simplifies the interface to complex subsystems, enhancing usability.
  • Reduces dependencies between client code and subsystems.

f. Flyweight Pattern

Intent: Use sharing to support large numbers of fine-grained objects efficiently.

Use Cases:

  • Implementing efficient storage for a large number of similar objects, such as characters in a text editor.
  • Reducing memory usage by sharing common data among objects.

Implementation in C++:

#include <iostream>
#include <unordered_map>
#include <memory>
#include <string>

// Flyweight
class Character {
public:
    Character(char symbol) : symbol(symbol) {}
    void display(int positionX, int positionY) const {
        std::cout << "Character '" << symbol << "' at (" << positionX << ", " << positionY << ").\n";
    }

private:
    char symbol;
};

// Flyweight Factory
class CharacterFactory {
public:
    std::shared_ptr<Character> getCharacter(char symbol) {
        auto it = characters.find(symbol);
        if (it != characters.end()) {
            return it->second;
        }
        auto character = std::make_shared<Character>(symbol);
        characters[symbol] = character;
        return character;
    }

private:
    std::unordered_map<char, std::shared_ptr<Character>> characters;
};

int main() {
    CharacterFactory factory;

    // Client code uses characters without worrying about their creation
    auto charA1 = factory.getCharacter('A');
    charA1->display(10, 20);

    auto charA2 = factory.getCharacter('A');
    charA2->display(15, 25);

    auto charB = factory.getCharacter('B');
    charB->display(20, 30);

    // Verify that charA1 and charA2 point to the same instance
    std::cout << "charA1 and charA2 are "
              << ((charA1 == charA2) ? "the same instance.\n" : "different instances.\n");

    return 0;
}

Output:

Character 'A' at (10, 20).
Character 'A' at (15, 25).
Character 'B' at (20, 30).
charA1 and charA2 are the same instance.

Key Points:

  • Flyweight minimizes memory usage by sharing common data among multiple objects.
  • Particularly useful when dealing with a large number of similar objects.

g. Proxy Pattern

Intent: Provide a surrogate or placeholder for another object to control access to it.

Use Cases:

  • Lazy initialization: Creating expensive objects on demand.
  • Access control: Restricting access to sensitive objects.
  • Remote proxies: Managing objects in different address spaces.

Implementation in C++:

#include <iostream>
#include <memory>
#include <string>

// Subject Interface
class Image {
public:
    virtual void display() = 0;
    virtual ~Image() = default;
};

// Real Subject
class RealImage : public Image {
public:
    RealImage(const std::string& filename) : filename(filename) {
        loadFromDisk();
    }

    void display() override {
        std::cout << "Displaying " << filename << ".\n";
    }

private:
    std::string filename;

    void loadFromDisk() {
        std::cout << "Loading " << filename << " from disk.\n";
    }
};

// Proxy
class ProxyImage : public Image {
public:
    ProxyImage(const std::string& filename) : filename(filename), realImage(nullptr) {}

    void display() override {
        if (!realImage) {
            realImage = std::make_unique<RealImage>(filename);
        }
        realImage->display();
    }

private:
    std::string filename;
    std::unique_ptr<RealImage> realImage;
};

int main() {
    std::unique_ptr<Image> image = std::make_unique<ProxyImage>("test_image.jpg");

    // Image will be loaded from disk only when display is called
    std::cout << "Image created.\n";
    image->display(); // Loads and displays the image
    image->display(); // Displays the image without loading again

    return 0;
}

Output:

Image created.
Loading test_image.jpg from disk.
Displaying test_image.jpg.
Displaying test_image.jpg.

Key Points:

  • Proxy controls access to the real object, providing additional functionality like lazy initialization or access control.
  • Enhances efficiency by delaying the creation of expensive objects until necessary.

4. Behavioral Patterns

Behavioral patterns are concerned with algorithms and the assignment of responsibilities between objects. They focus on how objects communicate and collaborate to achieve tasks.

a. Observer Pattern

Intent: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Use Cases:

  • Event handling systems.
  • Implementing distributed event systems.
  • Building model-view-controller (MVC) architectures.

Implementation in C++:

#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>

// Observer Interface
class Observer {
public:
    virtual void update(int state) = 0;
    virtual ~Observer() = default;
};

// Subject
class Subject {
public:
    void attach(std::shared_ptr<Observer> observer) {
        observers.emplace_back(observer);
    }

    void detach(std::shared_ptr<Observer> observer) {
        observers.erase(
            std::remove_if(observers.begin(), observers.end(),
                [&observer](const std::weak_ptr<Observer>& wp) {
                    auto sp = wp.lock();
                    return sp == observer;
                }),
            observers.end());
    }

    void notify() {
        for (auto it = observers.begin(); it != observers.end(); ) {
            if (auto sp = it->lock()) {
                sp->update(state);
                ++it;
            } else {
                // Remove expired weak_ptr
                it = observers.erase(it);
            }
        }
    }

    void setState(int newState) {
        state = newState;
        notify();
    }

private:
    std::vector<std::weak_ptr<Observer>> observers;
    int state;
};

// Concrete Observer
class ConcreteObserver : public Observer {
public:
    ConcreteObserver(const std::string& name) : name(name) {}
    void update(int state) override {
        std::cout << "Observer " << name << " notified. New state: " << state << "\n";
    }

private:
    std::string name;
};

int main() {
    Subject subject;

    auto observer1 = std::make_shared<ConcreteObserver>("A");
    auto observer2 = std::make_shared<ConcreteObserver>("B");

    subject.attach(observer1);
    subject.attach(observer2);

    subject.setState(10);
    subject.setState(20);

    // Detach observer1
    subject.detach(observer1);
    subject.setState(30);

    return 0;
}

Output:

Observer A notified. New state: 10
Observer B notified. New state: 10
Observer A notified. New state: 20
Observer B notified. New state: 20
Observer B notified. New state: 30

Key Points:

  • Observer allows objects to be notified of state changes without tight coupling.
  • Weak Pointers are used in the subject to prevent dangling references and memory leaks.

b. Strategy Pattern

Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

Use Cases:

  • Implementing different sorting algorithms.
  • Choosing different logging strategies.
  • Configuring varying compression algorithms.

Implementation in C++:

#include <iostream>
#include <memory>
#include <vector>
#include <algorithm>

// Strategy Interface
class SortingStrategy {
public:
    virtual void sort(std::vector<int>& data) = 0;
    virtual ~SortingStrategy() = default;
};

// Concrete Strategy: Bubble Sort
class BubbleSort : public SortingStrategy {
public:
    void sort(std::vector<int>& data) override {
        std::cout << "Sorting using Bubble Sort.\n";
        for (std::size_t i = 0; i < data.size(); ++i) {
            for (std::size_t j = 0; j < data.size() – i – 1; ++j) {
                if (data[j] > data[j + 1]) {
                    std::swap(data[j], data[j + 1]);
                }
            }
        }
    }
};

// Concrete Strategy: Quick Sort
class QuickSort : public SortingStrategy {
public:
    void sort(std::vector<int>& data) override {
        std::cout << "Sorting using Quick Sort.\n";
        quickSort(data, 0, data.size() – 1);
    }

private:
    void quickSort(std::vector<int>& data, int low, int high) {
        if (low < high) {
            int pi = partition(data, low, high);
            quickSort(data, low, pi – 1);
            quickSort(data, pi + 1, high);
        }
    }

    int partition(std::vector<int>& data, int low, int high) {
        int pivot = data[high];
        int i = low – 1;
        for (int j = low; j < high; ++j) {
            if (data[j] < pivot) {
                ++i;
                std::swap(data[i], data[j]);
            }
        }
        std::swap(data[i + 1], data[high]);
        return i + 1;
    }
};

// Context
class Sorter {
public:
    void setStrategy(std::unique_ptr<SortingStrategy> strategy) {
        this->strategy = std::move(strategy);
    }

    void sortData(std::vector<int>& data) {
        if (strategy) {
            strategy->sort(data);
        }
    }

private:
    std::unique_ptr<SortingStrategy> strategy;
};

int main() {
    Sorter sorter;
    std::vector<int> data = {5, 2, 9, 1, 5, 6};

    // Use Bubble Sort
    sorter.setStrategy(std::make_unique<BubbleSort>());
    sorter.sortData(data);
    for (const auto& num : data) {
        std::cout << num << " "; // Outputs sorted data
    }
    std::cout << "\n";

    // Reset data
    data = {5, 2, 9, 1, 5, 6};

    // Use Quick Sort
    sorter.setStrategy(std::make_unique<QuickSort>());
    sorter.sortData(data);
    for (const auto& num : data) {
        std::cout << num << " "; // Outputs sorted data
    }
    std::cout << "\n";

    return 0;
}

Output:

Sorting using Bubble Sort.
1 2 5 5 6 9
Sorting using Quick Sort.
1 2 5 5 6 9 

Key Points:

  • Strategy encapsulates interchangeable algorithms.
  • Promotes the Open/Closed Principle by allowing new strategies without modifying existing code.

c. Command Pattern

Intent: Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

Use Cases:

  • Implementing undo/redo functionality.
  • Queuing tasks.
  • Implementing callbacks and event handlers.

Implementation in C++:

#include <iostream>
#include <memory>
#include <vector>

// Command Interface
class Command {
public:
    virtual void execute() = 0;
    virtual void undo() = 0;
    virtual ~Command() = default;
};

// Receiver
class Light {
public:
    void on() {
        std::cout << "Light is ON.\n";
    }

    void off() {
        std::cout << "Light is OFF.\n";
    }
};

// Concrete Command: TurnOnCommand
class TurnOnCommand : public Command {
public:
    TurnOnCommand(std::shared_ptr<Light> light) : light(light) {}

    void execute() override {
        light->on();
    }

    void undo() override {
        light->off();
    }

private:
    std::shared_ptr<Light> light;
};

// Concrete Command: TurnOffCommand
class TurnOffCommand : public Command {
public:
    TurnOffCommand(std::shared_ptr<Light> light) : light(light) {}

    void execute() override {
        light->off();
    }

    void undo() override {
        light->on();
    }

private:
    std::shared_ptr<Light> light;
};

// Invoker
class RemoteControl {
public:
    void setCommand(std::unique_ptr<Command> cmd) {
        command = std::move(cmd);
    }

    void pressButton() {
        if (command) {
            command->execute();
            history.emplace_back(std::move(command));
        }
    }

    void pressUndo() {
        if (!history.empty()) {
            auto last = std::move(history.back());
            history.pop_back();
            last->undo();
        }
    }

private:
    std::unique_ptr<Command> command;
    std::vector<std::unique_ptr<Command>> history;
};

int main() {
    auto light = std::make_shared<Light>();
    RemoteControl remote;

    // Turn on the light
    remote.setCommand(std::make_unique<TurnOnCommand>(light));
    remote.pressButton();

    // Turn off the light
    remote.setCommand(std::make_unique<TurnOffCommand>(light));
    remote.pressButton();

    // Undo last action (turn off)
    remote.pressUndo(); // Light is ON.

    return 0;
}

Output:

Light is ON.
Light is OFF.
Light is ON.

Key Points:

  • Command encapsulates a request as an object.
  • Invoker (RemoteControl) can execute and undo commands without knowing the specifics.

d. Iterator Pattern

Intent: Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

Use Cases:

  • Iterating over collections like arrays, lists, or custom containers.
  • Providing multiple ways to traverse a collection.

Implementation in C++:

C++ already provides a robust iterator framework in the Standard Library. However, implementing a custom iterator demonstrates understanding of the pattern.

#include <iostream>
#include <vector>

// Custom Container
class CustomCollection {
public:
    void add(int value) {
        data.emplace_back(value);
    }

    // Iterator Class
    class Iterator {
    public:
        Iterator(std::vector<int>::iterator it) : it(it) {}

        int& operator*() {
            return *it;
        }

        Iterator& operator++() {
            ++it;
            return *this;
        }

        bool operator!=(const Iterator& other) const {
            return it != other.it;
        }

    private:
        std::vector<int>::iterator it;
    };

    Iterator begin() {
        return Iterator(data.begin());
    }

    Iterator end() {
        return Iterator(data.end());
    }

private:
    std::vector<int> data;
};

int main() {
    CustomCollection collection;
    collection.add(1);
    collection.add(2);
    collection.add(3);
    collection.add(4);
    collection.add(5);

    for (auto it = collection.begin(); it != collection.end(); ++it) {
        std::cout << *it << " "; // Outputs: 1 2 3 4 5
    }
    std::cout << "\n";

    return 0;
}

Output:

1 2 3 4 5 

Key Points:

  • Iterator provides a uniform way to traverse different collections.
  • C++'s iterator conventions enable seamless integration with Standard Library algorithms.

e. Mediator Pattern

Intent: Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly.

Use Cases:

  • Complex communication between multiple objects.
  • Reducing dependencies between communicating objects.

Implementation in C++:

#include <iostream>
#include <memory>
#include <string>

// Forward declarations
class Mediator;

// Colleague Interface
class Colleague {
public:
    Colleague(std::shared_ptr<Mediator> mediator) : mediator(mediator) {}
    virtual void send(const std::string& message) = 0;
    virtual void receive(const std::string& message) = 0;
    virtual ~Colleague() = default;

protected:
    std::shared_ptr<Mediator> mediator;
};

// Mediator Interface
class Mediator {
public:
    virtual void notify(std::shared_ptr<Colleague> sender, const std::string& event) = 0;
    virtual ~Mediator() = default;
};

// Concrete Colleague A
class ConcreteColleagueA : public Colleague {
public:
    ConcreteColleagueA(std::shared_ptr<Mediator> mediator) : Colleague(mediator) {}

    void send(const std::string& message) override {
        std::cout << "Colleague A sends: " << message << "\n";
        mediator->notify(shared_from_this(), message);
    }

    void receive(const std::string& message) override {
        std::cout << "Colleague A receives: " << message << "\n";
    }
};

// Concrete Colleague B
class ConcreteColleagueB : public Colleague {
public:
    ConcreteColleagueB(std::shared_ptr<Mediator> mediator) : Colleague(mediator) {}

    void send(const std::string& message) override {
        std::cout << "Colleague B sends: " << message << "\n";
        mediator->notify(shared_from_this(), message);
    }

    void receive(const std::string& message) override {
        std::cout << "Colleague B receives: " << message << "\n";
    }
};

// Concrete Mediator
class ConcreteMediator : public Mediator {
public:
    void registerColleagueA(std::shared_ptr<ConcreteColleagueA> colleague) {
        colleagueA = colleague;
    }

    void registerColleagueB(std::shared_ptr<ConcreteColleagueB> colleague) {
        colleagueB = colleague;
    }

    void notify(std::shared_ptr<Colleague> sender, const std::string& event) override {
        if (sender == colleagueA && colleagueB) {
            colleagueB->receive(event);
        } else if (sender == colleagueB && colleagueA) {
            colleagueA->receive(event);
        }
    }

private:
    std::shared_ptr<ConcreteColleagueA> colleagueA;
    std::shared_ptr<ConcreteColleagueB> colleagueB;
};

int main() {
    auto mediator = std::make_shared<ConcreteMediator>();
    auto colleagueA = std::make_shared<ConcreteColleagueA>(mediator);
    auto colleagueB = std::make_shared<ConcreteColleagueB>(mediator);

    mediator->registerColleagueA(colleagueA);
    mediator->registerColleagueB(colleagueB);

    colleagueA->send("Hello, B!");
    colleagueB->send("Hi, A!");

    return 0;
}

Output:

Colleague A sends: Hello, B!
Colleague B receives: Hello, B!
Colleague B sends: Hi, A!
Colleague A receives: Hi, A!

Key Points:

  • Mediator centralizes communication, reducing direct dependencies between colleagues.
  • Facilitates easier maintenance and scalability by managing interactions in one place.

f. Memento Pattern

Intent: Capture and externalize an object's internal state without violating encapsulation, allowing the object to be restored to this state later.

Use Cases:

  • Implementing undo/redo functionality.
  • Saving and restoring object states.

Implementation in C++:

#include <iostream>
#include <memory>
#include <string>

// Memento
class Memento {
public:
    Memento(const std::string& state) : state(state) {}
    std::string getState() const { return state; }

private:
    std::string state;
};

// Originator
class Originator {
public:
    void setState(const std::string& state) {
        std::cout << "Originator: Setting state to " << state << ".\n";
        this->state = state;
    }

    std::string getState() const { return state; }

    std::unique_ptr<Memento> saveStateToMemento() {
        std::cout << "Originator: Saving to Memento.\n";
        return std::make_unique<Memento>(state);
    }

    void getStateFromMemento(const Memento& memento) {
        state = memento.getState();
        std::cout << "Originator: State after restoring from Memento: " << state << ".\n";
    }

private:
    std::string state;
};

// Caretaker
class Caretaker {
public:
    void addMemento(std::unique_ptr<Memento> memento) {
        mementos.emplace_back(std::move(memento));
    }

    const Memento& getMemento(int index) const {
        return *mementos.at(index);
    }

private:
    std::vector<std::unique_ptr<Memento>> mementos;
};

int main() {
    Originator originator;
    Caretaker caretaker;

    originator.setState("State1");
    originator.setState("State2");
    caretaker.addMemento(originator.saveStateToMemento());

    originator.setState("State3");
    caretaker.addMemento(originator.saveStateToMemento());

    originator.setState("State4");

    originator.getStateFromMemento(caretaker.getMemento(0)); // Restores to State2
    originator.getStateFromMemento(caretaker.getMemento(1)); // Restores to State3

    return 0;
}

Output:

Originator: Setting state to State1.
Originator: Setting state to State2.
Originator: Saving to Memento.
Originator: Setting state to State3.
Originator: Saving to Memento.
Originator: Setting state to State4.
Originator: State after restoring from Memento: State2.
Originator: State after restoring from Memento: State3.

Key Points:

  • Memento stores the internal state of the Originator.
  • Caretaker manages multiple mementos without exposing their content.
  • Originator can restore its state from mementos.

g. State Pattern

Intent: Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.

Use Cases:

  • Implementing state machines.
  • Managing object states with distinct behaviors.

Implementation in C++:

#include <iostream>
#include <memory>
#include <string>

// Forward declaration
class Context;

// State Interface
class State {
public:
    virtual void handle(Context& context) = 0;
    virtual ~State() = default;
};

// Context
class Context {
public:
    Context(std::shared_ptr<State> initialState) : state(initialState) {}

    void setState(std::shared_ptr<State> newState) {
        state = newState;
    }

    void request() {
        if (state) {
            state->handle(*this);
        }
    }

private:
    std::shared_ptr<State> state;
};

// Concrete States
class ConcreteStateA : public State {
public:
    void handle(Context& context) override {
        std::cout << "State A handling request and switching to State B.\n";
        context.setState(std::make_shared<ConcreteStateB>());
    }
};

class ConcreteStateB : public State {
public:
    void handle(Context& context) override {
        std::cout << "State B handling request and switching to State A.\n";
        context.setState(std::make_shared<ConcreteStateA>());
    }
};

int main() {
    // Initialize context with State A
    Context context(std::make_shared<ConcreteStateA>());

    // Send requests and observe state transitions
    context.request(); // State A -> State B
    context.request(); // State B -> State A
    context.request(); // State A -> State B

    return 0;
}

Output:

State A handling request and switching to State B.
State B handling request and switching to State A.
State A handling request and switching to State B.

Key Points:

  • State encapsulates behavior associated with a particular state.
  • Context maintains a reference to a current state and delegates behavior to it.
  • Enhances maintainability by localizing state-specific behavior.

h. Template Method Pattern

Intent: Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing its structure.

Use Cases:

  • Implementing invariant parts of an algorithm while allowing variations.
  • Reusing code across multiple classes with similar algorithms.

Implementation in C++:

#include <iostream>

// Abstract Class with Template Method
class AbstractClass {
public:
    void templateMethod() {
        step1();
        step2();
        step3();
    }

    virtual ~AbstractClass() = default;

protected:
    virtual void step1() = 0;
    virtual void step2() = 0;

    // Common step
    void step3() {
        std::cout << "AbstractClass: Step 3 (common implementation).\n";
    }
};

// Concrete Class A
class ConcreteClassA : public AbstractClass {
protected:
    void step1() override {
        std::cout << "ConcreteClassA: Step 1 implementation.\n";
    }

    void step2() override {
        std::cout << "ConcreteClassA: Step 2 implementation.\n";
    }
};

// Concrete Class B
class ConcreteClassB : public AbstractClass {
protected:
    void step1() override {
        std::cout << "ConcreteClassB: Step 1 implementation.\n";
    }

    void step2() override {
        std::cout << "ConcreteClassB: Step 2 implementation.\n";
    }
};

int main() {
    std::unique_ptr<AbstractClass> classA = std::make_unique<ConcreteClassA>();
    classA->templateMethod();
    /*
    Output:
    ConcreteClassA: Step 1 implementation.
    ConcreteClassA: Step 2 implementation.
    AbstractClass: Step 3 (common implementation).
    */

    std::unique_ptr<AbstractClass> classB = std::make_unique<ConcreteClassB>();
    classB->templateMethod();
    /*
    Output:
    ConcreteClassB: Step 1 implementation.
    ConcreteClassB: Step 2 implementation.
    AbstractClass: Step 3 (common implementation).
    */

    return 0;
}

Output:

ConcreteClassA: Step 1 implementation.
ConcreteClassA: Step 2 implementation.
AbstractClass: Step 3 (common implementation).
ConcreteClassB: Step 1 implementation.
ConcreteClassB: Step 2 implementation.
AbstractClass: Step 3 (common implementation).

Key Points:

  • Template Method defines the algorithm structure in the base class.
  • Subclasses implement specific steps, allowing variations without altering the algorithm's structure.

i. Visitor Pattern

Intent: Represent an operation to be performed on elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.

Use Cases:

  • Performing operations across complex object structures.
  • Adding functionalities to classes without modifying them.

Implementation in C++:

#include <iostream>
#include <memory>
#include <vector>
#include <string>

// Forward declarations
class ConcreteElementA;
class ConcreteElementB;

// Visitor Interface
class Visitor {
public:
    virtual void visit(ConcreteElementA& element) = 0;
    virtual void visit(ConcreteElementB& element) = 0;
    virtual ~Visitor() = default;
};

// Element Interface
class Element {
public:
    virtual void accept(Visitor& visitor) = 0;
    virtual ~Element() = default;
};

// Concrete Elements
class ConcreteElementA : public Element {
public:
    void accept(Visitor& visitor) override {
        visitor.visit(*this);
    }

    std::string operationA() const {
        return "Operation A";
    }
};

class ConcreteElementB : public Element {
public:
    void accept(Visitor& visitor) override {
        visitor.visit(*this);
    }

    std::string operationB() const {
        return "Operation B";
    }
};

// Concrete Visitor
class ConcreteVisitor : public Visitor {
public:
    void visit(ConcreteElementA& element) override {
        std::cout << "ConcreteVisitor: " << element.operationA() << " processed.\n";
    }

    void visit(ConcreteElementB& element) override {
        std::cout << "ConcreteVisitor: " << element.operationB() << " processed.\n";
    }
};

int main() {
    std::vector<std::shared_ptr<Element>> elements;
    elements.emplace_back(std::make_shared<ConcreteElementA>());
    elements.emplace_back(std::make_shared<ConcreteElementB>());
    elements.emplace_back(std::make_shared<ConcreteElementA>());

    ConcreteVisitor visitor;
    for (auto& element : elements) {
        element->accept(visitor);
    }

    return 0;
}

Output:

ConcreteVisitor: Operation A processed.
ConcreteVisitor: Operation B processed.
ConcreteVisitor: Operation A processed.

Key Points:

  • Visitor allows adding new operations to existing object structures without modifying them.
  • Enhances flexibility by decoupling operations from object structures.

5. Implementing Common Design Patterns in C++

This section provides concrete implementations of some widely used design patterns in C++. These examples demonstrate the practical application of the patterns discussed above.

a. Singleton Pattern (Revisited)

Implementation with Thread Safety and Lazy Initialization:

#include <iostream>
#include <mutex>

class Singleton {
public:
    // Delete copy constructor and assignment operator
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // Public method to access the instance
    static Singleton& getInstance() {
        // Guaranteed to be thread-safe in C++11 and above
        static Singleton instance;
        return instance;
    }

    void showMessage() {
        std::cout << "Hello from Singleton!\n";
    }

private:
    // Private constructor
    Singleton() {
        std::cout << "Singleton instance created.\n";
    }

    // Private destructor
    ~Singleton() {
        std::cout << "Singleton instance destroyed.\n";
    }
};

int main() {
    Singleton::getInstance().showMessage();
    Singleton::getInstance().showMessage();
    return 0;
}

Output:

Singleton instance created.
Hello from Singleton!
Hello from Singleton!
Singleton instance destroyed.

Enhancements:

  • Thread Safety: Leveraging C++11's thread-safe static initialization.
  • Lazy Initialization: Instance is created only when getInstance is called.

b. Factory Method Pattern (Revisited)

Implementation with Parameterized Factory Method:

#include <iostream>
#include <memory>
#include <string>

// Product Interface
class Document {
public:
    virtual void open() = 0;
    virtual ~Document() = default;
};

// Concrete Products
class WordDocument : public Document {
public:
    void open() override {
        std::cout << "Opening Word Document.\n";
    }
};

class PDFDocument : public Document {
public:
    void open() override {
        std::cout << "Opening PDF Document.\n";
    }
};

// Creator Interface
class Application {
public:
    virtual std::unique_ptr<Document> createDocument(const std::string& type) = 0;
    virtual ~Application() = default;
};

// Concrete Creator
class OfficeApplication : public Application {
public:
    std::unique_ptr<Document> createDocument(const std::string& type) override {
        if (type == "Word") {
            return std::make_unique<WordDocument>();
        } else if (type == "PDF") {
            return std::make_unique<PDFDocument>();
        }
        return nullptr;
    }
};

int main() {
    std::unique_ptr<Application> app = std::make_unique<OfficeApplication>();

    auto doc1 = app->createDocument("Word");
    if (doc1) doc1->open(); // Outputs: Opening Word Document.

    auto doc2 = app->createDocument("PDF");
    if (doc2) doc2->open(); // Outputs: Opening PDF Document.

    auto doc3 = app->createDocument("Excel");
    if (doc3) doc3->open(); // No output, doc3 is nullptr

    return 0;
}

Output:

Opening Word Document.
Opening PDF Document.

Key Points:

  • Parameterized Factory Method allows creating different products based on input parameters.
  • Enhances flexibility and scalability by accommodating new product types without altering client code.

c. Observer Pattern (Revisited)

Implementation with Multiple Observers and State Tracking:

#include <iostream>
#include <vector>
#include <memory>
#include <string>

// Observer Interface
class Observer {
public:
    virtual void update(const std::string& message) = 0;
    virtual ~Observer() = default;
};

// Subject
class Subject {
public:
    void attach(std::shared_ptr<Observer> observer) {
        observers.emplace_back(observer);
    }

    void detach(std::shared_ptr<Observer> observer) {
        observers.erase(
            std::remove_if(observers.begin(), observers.end(),
                [&observer](const std::weak_ptr<Observer>& wp) {
                    auto sp = wp.lock();
                    return sp == observer;
                }),
            observers.end());
    }

    void notify(const std::string& message) {
        for (auto it = observers.begin(); it != observers.end(); ) {
            if (auto sp = it->lock()) {
                sp->update(message);
                ++it;
            } else {
                it = observers.erase(it);
            }
        }
    }

    void setState(const std::string& state) {
        this->state = state;
        notify(state);
    }

private:
    std::vector<std::weak_ptr<Observer>> observers;
    std::string state;
};

// Concrete Observer
class ConcreteObserver : public Observer {
public:
    ConcreteObserver(const std::string& name) : name(name) {}

    void update(const std::string& message) override {
        std::cout << "Observer " << name << " received state: " << message << "\n";
    }

private:
    std::string name;
};

int main() {
    Subject subject;

    auto observer1 = std::make_shared<ConcreteObserver>("A");
    auto observer2 = std::make_shared<ConcreteObserver>("B");
    auto observer3 = std::make_shared<ConcreteObserver>("C");

    subject.attach(observer1);
    subject.attach(observer2);
    subject.attach(observer3);

    subject.setState("State1");
    subject.setState("State2");

    // Detach observer2
    subject.detach(observer2);
    subject.setState("State3");

    return 0;
}

Output:

Observer A received state: State1
Observer B received state: State1
Observer C received state: State1
Observer A received state: State2
Observer B received state: State2
Observer C received state: State2
Observer A received state: State3
Observer C received state: State3

Key Points:

  • Multiple Observers can subscribe and unsubscribe dynamically.
  • State Changes in the subject trigger notifications to all active observers.

d. Strategy Pattern (Revisited)

Implementation with Multiple Strategies and Dynamic Selection:

#include <iostream>
#include <memory>
#include <vector>
#include <algorithm>

// Strategy Interface
class CompressionStrategy {
public:
    virtual void compress(const std::string& data) = 0;
    virtual ~CompressionStrategy() = default;
};

// Concrete Strategies
class ZipCompression : public CompressionStrategy {
public:
    void compress(const std::string& data) override {
        std::cout << "Compressing data using ZIP: " << data << "\n";
    }
};

class RarCompression : public CompressionStrategy {
public:
    void compress(const std::string& data) override {
        std::cout << "Compressing data using RAR: " << data << "\n";
    }
};

class TarCompression : public CompressionStrategy {
public:
    void compress(const std::string& data) override {
        std::cout << "Compressing data using TAR: " << data << "\n";
    }
};

// Context
class Compressor {
public:
    void setStrategy(std::unique_ptr<CompressionStrategy> strategy) {
        this->strategy = std::move(strategy);
    }

    void compressData(const std::string& data) {
        if (strategy) {
            strategy->compress(data);
        } else {
            std::cout << "No compression strategy set.\n";
        }
    }

private:
    std::unique_ptr<CompressionStrategy> strategy;
};

int main() {
    Compressor compressor;

    compressor.compressData("Sample Data"); // No strategy set.

    compressor.setStrategy(std::make_unique<ZipCompression>());
    compressor.compressData("Sample Data"); // Uses ZIP.

    compressor.setStrategy(std::make_unique<RarCompression>());
    compressor.compressData("Sample Data"); // Uses RAR.

    compressor.setStrategy(std::make_unique<TarCompression>());
    compressor.compressData("Sample Data"); // Uses TAR.

    return 0;
}

Output:

No compression strategy set.
Compressing data using ZIP: Sample Data
Compressing data using RAR: Sample Data
Compressing data using TAR: Sample Data

Key Points:

  • Dynamic Strategy Selection allows changing algorithms at runtime.
  • Promotes flexibility and reusability by decoupling algorithms from the context.

e. Observer Pattern in Real-World Applications

To demonstrate a more realistic scenario, consider implementing a simple event system using the Observer pattern.

Implementation in C++:

#include <iostream>
#include <vector>
#include <memory>
#include <functional>

// Event System
class Event {
public:
    using HandlerType = std::function<void(int)>;

    void subscribe(HandlerType handler) {
        handlers.emplace_back(handler);
    }

    void notify(int data) {
        for (auto& handler : handlers) {
            handler(data);
        }
    }

private:
    std::vector<HandlerType> handlers;
};

// Subscriber Classes
class SubscriberA {
public:
    void onEvent(int data) {
        std::cout << "Subscriber A received data: " << data << "\n";
    }
};

class SubscriberB {
public:
    void onEvent(int data) {
        std::cout << "Subscriber B received data: " << data << "\n";
    }
};

int main() {
    Event event;

    SubscriberA subA;
    SubscriberB subB;

    // Subscribe member functions using lambdas
    event.subscribe([&subA](int data) { subA.onEvent(data); });
    event.subscribe([&subB](int data) { subB.onEvent(data); });

    // Trigger events
    event.notify(100);
    event.notify(200);

    return 0;
}

Output:

Subscriber A received data: 100
Subscriber B received data: 100
Subscriber A received data: 200
Subscriber B received data: 200

Key Points:

  • Functional Handlers: Leveraging std::function allows flexibility in handling events.
  • Lambda Expressions provide a concise way to bind member functions as event handlers.

Best Practices and Considerations

  1. Understand When to Use Each Pattern: Not every problem requires a design pattern. Assess the problem's nature before applying a pattern.
  2. Favor Composition Over Inheritance: Many structural patterns promote composition to enhance flexibility and reduce tight coupling.
  3. Maintain Single Responsibility Principle: Ensure that classes focused on a single responsibility to prevent bloated implementations.
  4. Encapsulate Varying Behavior: Use behavioral patterns like Strategy and State to manage varying behaviors dynamically.
  5. Promote Loose Coupling: Patterns like Observer and Mediator reduce direct dependencies between classes, enhancing maintainability.
  6. Leverage C++ Features: Utilize modern C++ features such as smart pointers, lambda expressions, and templates to implement patterns more effectively.
  7. Avoid Overcomplicating Code: Applying patterns unnecessarily can lead to overcomplicated and less readable code.
  8. Document Pattern Usage: Clearly document when and why a particular pattern is used to aid future maintenance and understanding.
  9. Consider Performance Implications: Some patterns introduce overhead (e.g., Observer with multiple notifications). Assess their impact on performance-critical applications.
  10. Test Thoroughly: Patterns can introduce complex interactions. Ensure thorough testing to validate correct behavior.

Conclusion

Design patterns are invaluable tools in a C++ developer's arsenal, providing standardized solutions to common design challenges. By understanding and effectively implementing creational, structural, and behavioral patterns, developers can create robust, flexible, and maintainable software systems.

In C++, leveraging the language's powerful features—such as templates, smart pointers, and the Standard Library—enhances the implementation of these patterns, making them more efficient and expressive. However, it's crucial to apply design patterns judiciously, ensuring that their use aligns with the problem's context and contributes to the system's overall design goals.

Embracing design patterns fosters better software architecture, promotes code reuse, and facilitates communication among developers by providing a shared vocabulary of design concepts. As C++ continues to evolve, integrating these patterns with modern language features will empower developers to build sophisticated and high-performance applications.

Leave a Reply