Design Patterns in C++

In the field of software development, design patterns are general solutions to recurring problems. Mastering design patterns can not only improve code quality but also facilitate more efficient collaboration with other developers.

1. Singleton Pattern: Ensuring a Global Unique Instance

Definition: The Singleton pattern ensures that a class has only one instance and provides a global access point to it.

Application Scenarios:

  • Logger

  • Configuration Manager

  • Database Connection Pool

Meyers’ Singleton Implementation (C++11+)

class Logger {public:    // Static method to get the singleton instance    static Logger& getInstance() {        static Logger instance; // C++11 guarantees thread safety        return instance;    }    // Disable copy constructor and assignment operator    Logger(const Logger&) = delete;    Logger& operator=(const Logger&) = delete;    // Business method    void log(const std::string& message) {        std::cout << "[LOG] " << message << std::endl;    }private:    Logger() = default; // Private constructor    ~Logger() = default;};// Usage examplevoid example() {    Logger::getInstance().log("System started");}

Technical Points:

  1. Static local variable initialized on first call (thread-safe since C++11)

  2. Deleting copy constructor and assignment operator prevents cloning

  3. Private constructor ensures no external instantiation

2. Factory Pattern: Decoupling Object Creation Logic

Definition: The Factory pattern encapsulates the object creation logic within a factory class/method, achieving separation of creation and usage.

Application Scenarios:

  1. Complex object creation process

  2. Dynamic object creation based on conditions

  3. Framework extension point design

Simple Factory Implementation

// Abstract product classclass Shape {public:    virtual void draw() = 0;    virtual ~Shape() = default;};// Concrete product classclass Circle : public Shape {public:    void draw() override { std::cout << "Drawing Circle" << std::endl; }};class Square : public Shape {public:    void draw() override { std::cout << "Drawing Square" << std::endl; }};// Factory classclass ShapeFactory {public:    static std::unique_ptr<Shape> createShape(const std::string& type) {        if (type == "circle") return std::make_unique<Circle>();        if (type == "square") return std::make_unique<Square>();        throw std::invalid_argument("Invalid shape type");    }};// Usage examplevoid factoryExample() {    auto circle = ShapeFactory::createShape("circle");    circle->draw();}

Advanced Techniques:

  • Use enums instead of string parameters

  • Combine templates to implement parameterized factories

  • Registration-based factories support dynamic extension

3. Observer Pattern: Building Event-Driven Systems

Definition: The Observer pattern defines a one-to-many dependency, so that when one object’s state changes, all its dependents are notified.

Application Scenarios:

  • Message notification systems

  • GUI event handling

  • Status monitoring systems

Standard Implementation

#include <vector>#include <memory>#include <string>// Observer interfaceclass Observer {public:    virtual void update(const std::string& message) = 0;    virtual ~Observer() = default;};// Subject interfaceclass Subject {public:    virtual void attach(std::shared_ptr<Observer> observer) = 0;    virtual void detach(std::shared_ptr<Observer> observer) = 0;    virtual void notify(const std::string& message) = 0;    virtual ~Subject() = default;};// Concrete subjectclass NewsPublisher : public Subject {private:    std::vector<std::shared_ptr<Observer>> observers;    std::string latestNews;public:    void attach(std::shared_ptr<Observer> observer) override {        observers.push_back(observer);    }    void detach(std::shared_ptr<Observer> observer) override {        observers.erase(            std::remove(observers.begin(), observers.end(), observer),            observers.end()        );    }    void notify(const std::string& message) override {        for (auto& observer : observers) {            observer->update(message);        }    }    void publishNews(const std::string& news) {        latestNews = news;        notify(latestNews);    }};// Concrete observerclass NewsSubscriber : public Observer {private:    std::string name;public:    explicit NewsSubscriber(const std::string& name) : name(name) {}    void update(const std::string& message) override {        std::cout << name << " received: " << message << std::endl;    }};// Usage examplevoid observerExample() {    auto publisher = std::make_shared<NewsPublisher>();    auto subscriber1 = std::make_shared<NewsSubscriber>("Alice");    auto subscriber2 = std::make_shared<NewsSubscriber>("Bob");    publisher->attach(subscriber1);    publisher->attach(subscriber2);    publisher->publishNews("Breaking: Design Patterns Explained!");}

Advanced Applications:

  • Use smart pointers to manage observer lifecycles

  • Implement multi-level observer hierarchy

  • Asynchronous notification mechanisms to improve performance

4. Strategy Pattern: Encapsulating Interchangeable Algorithms

Definition: The Strategy pattern encapsulates algorithms into independent strategy classes, allowing them to be interchangeable and making algorithm changes independent of the client.

Application Scenarios:

  • Switching sorting algorithms

  • Choosing payment methods

  • Switching compression algorithms

Strategy Pattern Implementation:

#include <vector>#include <memory>#include <iostream>// Strategy interfaceclass SortStrategy {public:    virtual void sort(std::vector<int>& data) = 0;    virtual ~SortStrategy() = default;};// Concrete strategyclass QuickSort : public SortStrategy {public:    void sort(std::vector<int>& data) override {        std::cout << "Sorting using QuickSort" << std::endl;        // Implement quicksort logic    }};class MergeSort : public SortStrategy {public:    void sort(std::vector<int>& data) override {        std::cout << "Sorting using MergeSort" << std::endl;        // Implement mergesort logic    }};// Context classclass Sorter {private:    std::unique_ptr<SortStrategy> strategy;public:    explicit Sorter(std::unique_ptr<SortStrategy> strategy)        : strategy(std::move(strategy)) {}    void setStrategy(std::unique_ptr<SortStrategy> newStrategy) {        strategy = std::move(newStrategy);    }    void executeSort(std::vector<int>& data) {        strategy->sort(data);    }};// Usage examplevoid strategyExample() {    std::vector<int> data = {5, 3, 8, 1, 4};    // Using quicksort    Sorter sorter(std::make_unique<QuickSort>());    sorter.executeSort(data);    // Dynamically switch to mergesort    sorter.setStrategy(std::make_unique<MergeSort>());    sorter.executeSort(data);}

Design Advantages:

  • Follows the Open/Closed Principle (open for extension, closed for modification)

  • Eliminates a large number of conditional statements

  • Facilitates unit testing of different strategies

5. Summary and Best Practices

1. Singleton Pattern:

  • Use Meyers’ implementation to ensure thread safety

  • Avoid using locking mechanisms in performance-sensitive scenarios

2. Factory Pattern:

  • Simple factories can be used for small projects

  • Large frameworks are recommended to use abstract factories

3. Observer Pattern:

  • Be cautious to avoid circular references that lead to memory leaks

  • Consider using weak references to manage observers

4. Strategy Pattern:

  • Combine with the factory pattern to dynamically create strategies

  • Consider using function objects instead of strategy classes

Design patterns are a summary of experience rather than dogma; they should be flexibly applied according to needs in actual development, avoiding over-design.

Leave a Comment