C++: Design Patterns – With Clear and Practical Examples


:large_blue_diamond: C++: Design Patterns – With Clear and Practical Examples

Design patterns are proven solutions to common software design problems. In C++, which is a multi-paradigm language with strong support for object-oriented programming, design patterns are often used to create robust, maintainable, and scalable applications.

In this article, we’ll look at what design patterns are, explore their types, and provide clear C++ examples of three common ones:

  • Singleton Pattern
  • Factory Pattern
  • Observer Pattern

:books: What Are Design Patterns?

Design patterns are reusable templates or blueprints for solving software design problems. They aren’t finished code, but rather best practices refined by developers over time.

Design patterns fall into three main categories:

  1. Creational Patterns – Deal with object creation (e.g., Singleton, Factory)
  2. Structural Patterns – Deal with object composition (e.g., Adapter, Composite)
  3. Behavioral Patterns – Deal with communication between objects (e.g., Observer, Strategy)

:brick: 1. Singleton Pattern

:white_check_mark: Purpose:

Ensure a class has only one instance and provides a global point of access to it.

:package: Example:

#include <iostream>

class Singleton {
private:
    static Singleton* instance;
    Singleton() {}  // Private constructor

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

    static Singleton* getInstance() {
        if (!instance)
            instance = new Singleton();
        return instance;
    }

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

// Initialize static member
Singleton* Singleton::instance = nullptr;

int main() {
    Singleton* s1 = Singleton::getInstance();
    Singleton* s2 = Singleton::getInstance();

    s1->showMessage();

    // Prove they are the same
    std::cout << (s1 == s2 ? "Same instance" : "Different instances") << std::endl;
    return 0;
}

:brain: Output:

Hello from Singleton!
Same instance

:factory: 2. Factory Pattern

:white_check_mark: Purpose:

Create objects without specifying the exact class of object that will be created.

:package: Example:

#include <iostream>
#include <memory>

class Product {
public:
    virtual void use() = 0;
};

class ConcreteProductA : public Product {
public:
    void use() override {
        std::cout << "Using Product A" << std::endl;
    }
};

class ConcreteProductB : public Product {
public:
    void use() override {
        std::cout << "Using Product B" << std::endl;
    }
};

class ProductFactory {
public:
    static std::unique_ptr<Product> createProduct(const std::string& type) {
        if (type == "A")
            return std::make_unique<ConcreteProductA>();
        else if (type == "B")
            return std::make_unique<ConcreteProductB>();
        else
            return nullptr;
    }
};

int main() {
    auto product = ProductFactory::createProduct("A");
    if (product) product->use();

    product = ProductFactory::createProduct("B");
    if (product) product->use();

    return 0;
}

:brain: Output:

Using Product A
Using Product B

:bell: 3. Observer Pattern

:white_check_mark: Purpose:

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

:package: Example:

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

// Forward declaration
class Observer;

class Subject {
private:
    std::vector<Observer*> observers;
    int state;

public:
    void attach(Observer* observer) {
        observers.push_back(observer);
    }

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

    int getState() const {
        return state;
    }

    void notify();
};

class Observer {
public:
    virtual void update(Subject* subject) = 0;
};

void Subject::notify() {
    for (auto observer : observers) {
        observer->update(this);
    }
}

class ConcreteObserver : public Observer {
private:
    std::string name;

public:
    ConcreteObserver(const std::string& n) : name(n) {}

    void update(Subject* subject) override {
        std::cout << "Observer " << name << " received update: " << subject->getState() << std::endl;
    }
};

int main() {
    Subject subject;

    ConcreteObserver o1("A"), o2("B");
    subject.attach(&o1);
    subject.attach(&o2);

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

    return 0;
}

:brain: Output:

Observer A received update: 10
Observer B received update: 10
Observer A received update: 20
Observer B received update: 20

:brain: Summary

Pattern Category Purpose
Singleton Creational One instance, global access
Factory Creational Create objects without knowing exact class
Observer Behavioral Notify multiple objects of state changes

:rocket: Final Thoughts

Understanding design patterns is crucial for writing clean, maintainable, and scalable C++ code. While these examples are simple, real-world usage often involves combining patterns to create complex architectures.