Design Patterns in C++

repository·main·Indexed 18 days ago

https://github.com/refactoringguru/design-patterns-cpp

A collection of C++17 implementations for classic Gang of Four (GoF) design patterns. The repository provides both conceptual examples focusing on internal structure and RealWorld examples demonstrating practical application. It includes detailed implementations of patterns such as Abstract Factory, Adapter (via multiple inheritance and composition), and Bridge.

Tokens
26.7K
Snippets
65
Records
80
Agent score
63%

What's inside refactoringguru-design-patterns-cpp

  1. Overview of Design Patterns in C++ examples

    main

    This repository provides C++ implementations of all classic GoF (Gang of Four) design patterns. The examples are categorized into two types:

    • Conceptual examples: Focus on the internal structure of the pattern, accompanied by detailed comments to explain the mechanics.
    • RealWorld examples: Demonstrate how the patterns are applied within a practical C++ application context.
  2. Requirements for running C++ examples

    main

    The examples are designed as cross-platform console applications using the C++17 standard. To run them, you need:

    1. A C++ compiler that supports C++17 (e.g., g++).
    2. A development environment (Visual Studio Code is recommended).

    If using Visual Studio Code, it is recommended to install the C++ extension for code editing, navigation, and debugging.

  3. Configure VSCode task to build examples with g++

    main

    To compile the examples in VSCode using g++, create a .vscode/tasks.json file and add a build task. The following configuration targets a specific conceptual example (e.g., Conceptual/main.cc) and compiles it using the C++17 standard with debug symbols enabled.

    {
        "version": "2.0.0",
        "tasks": [
            {
                "label": "build",
                "type": "shell",
                "command": "g++ -g -std=c++17 Conceptual/main.cc -o main",
                "group":{
                    "kind": "build",
                    "isDefault": true    
                },
                "problemMatcher":"$gcc"
            }
        ]
    }
  4. The Bridge Design Pattern

    main

    The Bridge pattern decouples an abstraction from its implementation so that the two can vary independently. It splits a large class or a set of closely related classes into two separate hierarchies:

    1. Abstraction: Defines the interface for the 'control' part of the hierarchy. It maintains a reference to an object of the Implementation hierarchy and delegates work to it.
    2. Implementation: Defines the interface for all concrete implementation classes. This interface often provides primitive operations, while the Abstraction defines higher-level operations based on those primitives.

    By using this pattern, you can extend the Abstraction without changing the Implementation classes, and vice versa. Client code should ideally only depend on the Abstraction class, allowing it to support any combination of abstraction and implementation.

    // Conceptual structure:
    // Abstraction (High-level) ----> Implementation (Low-level/Platform)
    //      |                               | 
    // ExtendedAbstraction            ConcreteImplementationA
    //                                | 
    //                             ConcreteImplementationB
  5. Implement the Template Method pattern

    main

    The Template Method pattern defines the skeleton of an algorithm in a superclass, allowing subclasses to override specific steps without changing the algorithm's overall structure.

    To implement this pattern:

    1. Create an Abstract Class that defines a TemplateMethod() containing the sequence of algorithm steps.
    2. In the abstract class, define:
      • Base Operations: Methods with default implementations that perform the bulk of the work.
      • Required Operations: Pure virtual methods (= 0) that subclasses must implement.
      • Hooks: Virtual methods with empty default implementations that subclasses may optionally override to provide additional extension points.
    3. Create Concrete Classes that inherit from the abstract class and implement the required operations.
    class AbstractClass {
     public:
      void TemplateMethod() const {
        this->BaseOperation1();
        this->RequiredOperations1();
        this->BaseOperation2();
        this->Hook1();
        this->RequiredOperation2();
        this->BaseOperation3();
        this->Hook2();
      }
    
     protected:
      void BaseOperation1() const { /* ... */ }
      void BaseOperation2() const { /* ... */ }
      void BaseOperation3() const { /* ... */ }
    
      virtual void RequiredOperations1() const = 0;
      virtual void RequiredOperation2() const = 0;
    
      virtual void Hook1() const {} // Optional extension point
      virtual void Hook2() const {} // Optional extension point
    };
    
    class ConcreteClass1 : public AbstractClass {
     protected:
      void RequiredOperations1() const override { /* implementation */ }
      void RequiredOperation2() const override { /* implementation */ }
    };
  6. Implement the Mediator pattern to reduce object coupling

    main

    The Mediator pattern reduces chaotic dependencies between objects by restricting direct communication between them and forcing them to collaborate only via a mediator object.

    To implement this pattern, you need four main parts:

    1. Mediator Interface: Declares a Notify method used by components to signal events.
    2. Base Component: A base class that stores a pointer to the Mediator and provides a set_mediator method.
    3. Concrete Components: Classes that implement specific functionality. They do not depend on other components; instead, they call mediator_->Notify(this, "event_name") when an action occurs.
    4. Concrete Mediator: Implements the coordination logic. It holds references to the concrete components and implements the Notify method to react to specific events by triggering operations on other components.
    // Example of how components notify the mediator
    class Component1 : public BaseComponent {
     public:
      void DoA() {
        std::cout << "Component 1 does A.\n";
        this->mediator_->Notify(this, "A");
      }
    };
    
    // Example of how the mediator coordinates components
    class ConcreteMediator : public Mediator {
     private:
      Component1 *component1_;
      Component2 *component2_;
    
     public:
      void Notify(BaseComponent *sender, std::string event) const override {
        if (event == "A") {
          // React to event A by triggering an action in component 2
          this->component2_->DoC();
        }
      }
    };
  7. Implement the Proxy Design Pattern

    main

    The Proxy pattern provides a surrogate or placeholder for another object to control access to it or add additional responsibilities (like logging or access control) without modifying the original object's code.

    Core Components

    • Subject (Interface): Declares common operations for both the RealSubject and the Proxy. This allows the client to treat both objects interchangeably.
    • RealSubject: Contains the core business logic. It performs the actual work requested by the client.
    • Proxy: Maintains a reference to a RealSubject. It implements the same interface as the RealSubject and intercepts calls to perform pre-processing (e.g., CheckAccess()) or post-processing (e.g., LogAccess()) before or after delegating the call to the real subject.

    Usage Pattern

    To use the pattern effectively, the client code should interact with objects via the Subject interface. This ensures that the client can work with either a RealSubject or a Proxy without knowing the difference.

    // The Subject interface
    class Subject {
     public:
      virtual void Request() const = 0;
    };
    
    // The RealSubject containing core logic
    class RealSubject : public Subject {
     public:
      void Request() const override {
        std::cout << "RealSubject: Handling request.\n";
      }
    };
    
    // The Proxy controlling access
    class Proxy : public Subject {
     private:
      RealSubject *real_subject_;
      bool CheckAccess() const { /* ... */ return true; }
      void LogAccess() const { /* ... */ }
     public:
      Proxy(RealSubject *real_subject) : real_subject_(new RealSubject(*real_subject)) {}
      ~Proxy() { delete real_subject_; }
      void Request() const override {
        if (this->CheckAccess()) {
          this->real_subject_->Request();
          this->LogAccess();
        }
      }
    };
    
    // Client code using the Subject interface
    void ClientCode(const Subject &subject) {
      subject.Request();
    }
  8. How the Memento pattern works

    main

    The Memento pattern allows you to save and restore an object's internal state without violating encapsulation. It involves three main components:

    1. Originator: The object whose state needs to be saved. It creates a Memento containing a snapshot of its current state and uses a Memento to restore itself.
    2. Memento: An interface (or object) that stores the state. It provides metadata (like creation date) to external observers but keeps the actual state data hidden from everyone except the Originator.
    3. Caretaker: Responsible for the memento's storage (e.g., a history stack). It never operates on or examines the contents of the memento; it simply triggers the Save and Restore operations.
    // Conceptual workflow
    Originator *originator = new Originator("initial state");
    Caretaker *caretaker = new Caretaker(originator);
    
    caretaker->Backup(); // Saves state
    originator->DoSomething(); // Changes state
    caretaker->Undo(); // Restores state
  9. How the Flyweight pattern works

    main

    The Flyweight pattern is used to fit more objects into available RAM by sharing common parts of state between multiple objects. It distinguishes between two types of state:

    1. Intrinsic State (SharedState): This is the common portion of the state that belongs to multiple entities. It is stored within the Flyweight object.
    2. Extrinsic State (UniqueState): This is the state that is unique to each entity. It is not stored in the Flyweight but is passed to the Flyweight's methods (e.g., via Operation()) when needed.

    By separating these, you avoid duplicating heavy, shared data across thousands of individual objects.

    // Intrinsic state (shared)
    struct SharedState {
        std::string brand_;
        std::string model_;
        std::string color_;
    };
    
    // Extrinsic state (unique)
    struct UniqueState {
        std::string owner_;
        std::string plates_;
    };
    
    // The Flyweight uses both
    class Flyweight {
    public:
        void Operation(const UniqueState &unique_state) const;
    };
  10. Implement a thread-safe Singleton in C++

    main

    The Singleton pattern ensures a class has only one instance while providing a global access point. To make it thread-safe, use a std::mutex and std::lock_guard within the static access method to prevent multiple threads from creating separate instances simultaneously.

    Key implementation requirements:

    • Private Constructor/Destructor: Prevents direct use of new or delete by clients.
    • Deleted Copy Constructor and Assignment Operator: Ensures the singleton cannot be cloned or assigned.
    • Static Access Method: A method like GetInstance() that manages the lifecycle of the single instance.
    • Thread Safety: Use a mutex to protect the critical section where the instance is checked for null and subsequently initialized.
    #include <iostream>
    #include <mutex>
    #include <thread>
    
    class Singleton
    {
    private:
        static Singleton * pinstance_;
        static std::mutex mutex_;
    
    protected:
        Singleton(const std::string value): value_(value) {}
        ~Singleton() {}
        std::string value_;
    
    public:
        Singleton(Singleton &other) = delete;
        void operator=(const Singleton &) = delete;
    
        static Singleton *GetInstance(const std::string& value);
    
        void SomeBusinessLogic() {}
        std::string value() const { return value_; }
    };
    
    Singleton* Singleton::pinstance_{nullptr};
    std::mutex Singleton::mutex_;
    
    Singleton *Singleton::GetInstance(const std::string& value)
    {
        std::lock_guard<std::mutex> lock(mutex_);
        if (pinstance_ == nullptr)
        {
            pinstance_ = new Singleton(value);
        }
        return pinstance_;
    }
  11. Implement the Observer Design Pattern

    main

    The Observer pattern defines a subscription mechanism to notify multiple objects (Observers) about any events that happen to the object they're observing (the Subject/Publisher).

    To implement this pattern, you need two primary interfaces:

    1. IObserver: Defines the Update method that the Subject calls to notify the observer of a change.
    2. ISubject: Defines methods for managing subscriptions: Attach(IObserver *observer), Detach(IObserver *observer), and Notify().

    In a typical implementation, the Subject maintains a list of observers and iterates through them to call their Update methods whenever its state changes.

    class IObserver {
     public:
      virtual ~IObserver(){};
      virtual void Update(const std::string &message_from_subject) = 0;
    };
    
    class ISubject {
     public:
      virtual ~ISubject(){};
      virtual void Attach(IObserver *observer) = 0;
      virtual void Detach(IObserver *observer) = 0;
      virtual void Notify() = 0;
    };
  12. Implement a modern Visitor pattern using std::variant and std::visit

    main

    In modern C++17, the Visitor pattern can be implemented without traditional class hierarchies or virtual functions by using std::variant to represent a type-safe union of possible types and std::visit to apply a visitor functor.

    This approach follows the Open-Closed Principle more effectively because the data classes (e.g., Food, Drink) do not need to inherit from a base class or implement an AcceptVisitor method. This allows you to add new types to the std::variant or new visitors without modifying the existing data structures.

    // 1. Define data structures
    class Food { /* ... */ };
    class Drink { /* ... */ };
    
    // 2. Create a variant union of all possible types
    using Item = std::variant<Food, Drink>;
    using Menu = std::vector<Item>;
    
    // 3. Create a visitor functor with overloaded operator()
    class Serialiser {
    public:
        void operator()(Food const &food) const { /* handle food */ }
        void operator()(Drink const &drink) const { /* handle drink */ }
    };
    
    // 4. Apply the visitor using std::visit
    Menu menu;
    menu.emplace_back(Food{"Borscht", 160, Food::Label::meat});
    std::visit(Serialiser{}, menu[0]);