Design Patterns

Design patterns are reusable solutions to commonly occurring problems in software design. They represent best practices and provide a blueprint for solving specific design problems in a structured and efficient manner. In C++, design patterns can be implemented using various language features and idioms.

What are Design Patterns?

In software engineering, design patterns are well-established, reusable solutions to commonly encountered programming problems. They provide a general blueprint for structuring code that can be applied in various contexts.

Benefits of Design Patterns

  • Improved Code Reusability: Patterns promote code reuse by offering pre-defined solutions that can be adapted to different situations.
  • Enhanced Maintainability: Code that follows design patterns is often easier to understand, modify, and debug due to its well-defined structure.
  • Promotes Communication: Design patterns provide a common vocabulary for developers, facilitating communication and collaboration within a team.

Types of Design Patterns

Design patterns are typically categorized into three main groups

  1. Creational Patterns: These patterns focus on object creation mechanisms, promoting flexibility and decoupling object creation from their usage. Examples include Singleton, Factory Method, and Builder patterns.
  2. Structural Patterns: They deal with the composition of classes and objects, influencing how classes and objects work together. Examples include Adapter, Bridge, and Composite patterns.
  3. Behavioral Patterns: These patterns define communication patterns between objects and how they interact to achieve a specific behavior. Examples include Observer, Strategy, and Command patterns.

Common Creational Design Patterns

Singleton Pattern

  • Intent: Ensures a class has only one instance and provides a global access point to it.
  • Problem: Sometimes, you need a class to have exactly one instance throughout the program. However, creating the instance directly might lead to tight coupling and difficulty in controlling its creation.
  • Solution: The Singleton pattern defines a class that controls the instantiation of itself, providing a global access point to the single instance.
				
					#include <iostream>

class Singleton {
public:
    // Static member function to access the singleton instance
    static Singleton& getInstance() {
        static Singleton instance; // Static instance created only once
        return instance;
    }

    // Other member functions and variables
    void showMessage() {
        std::cout << "Hello from Singleton!" << std::endl;
    }

private:
    // Private constructor to prevent instantiation
    Singleton() {} // Private constructor

    // Private destructor to prevent deletion
    ~Singleton() {}
};

int main() {
    // Get the singleton instance
    Singleton& singleton = Singleton::getInstance();

    // Use the singleton instance
    singleton.showMessage();

    return 0;
}

				
			

In this example:

  • The Singleton class has a private constructor, preventing external instantiation.
  • getInstance() is a static member function that provides access to the singleton instance. It creates the instance on the first call and returns the same instance on subsequent calls.
  • The main() function demonstrates how to use the singleton by obtaining its instance and invoking a member function (showMessage() in this case).

Factory Method Pattern

  • Intent: Defines an interface for creating objects but lets subclasses decide which class to instantiate.
  • Problem: If your code directly creates objects of a specific concrete class, it becomes tightly coupled to that implementation. This makes it difficult to change the type of object created in the future.
  • Solution: The Factory Method pattern introduces a factory class that defines an interface for creating objects. Subclasses of the factory can then override this interface to create objects of specific concrete classes.
				
					#include <iostream>

// Abstract base class for vehicles
class Vehicle {
public:
    virtual void drive() const = 0;
};

// Concrete Car class
class Car : public Vehicle {
public:
    void drive() const override {
        std::cout << "Driving a car" << std::endl;
    }
};

// Concrete Bicycle class
class Bicycle : public Vehicle {
public:
    void drive() const override {
        std::cout << "Riding a bicycle" << std::endl;
    }
};

// Factory method to create vehicles
class VehicleFactory {
public:
    // Factory method to create vehicles
    virtual Vehicle* createVehicle() const = 0;
};

// Concrete factory for creating cars
class CarFactory : public VehicleFactory {
public:
    Vehicle* createVehicle() const override {
        return new Car();
    }
};

// Concrete factory for creating bicycles
class BicycleFactory : public VehicleFactory {
public:
    Vehicle* createVehicle() const override {
        return new Bicycle();
    }
};

int main() {
    // Create a car factory
    VehicleFactory* carFactory = new CarFactory();
    
    // Use the car factory to create a car
    Vehicle* car = carFactory->createVehicle();
    car->drive(); // Output: Driving a car
    
    // Clean up memory
    delete car;
    delete carFactory;
    
    // Create a bicycle factory
    VehicleFactory* bicycleFactory = new BicycleFactory();
    
    // Use the bicycle factory to create a bicycle
    Vehicle* bicycle = bicycleFactory->createVehicle();
    bicycle->drive(); // Output: Riding a bicycle
    
    // Clean up memory
    delete bicycle;
    delete bicycleFactory;
    
    return 0;
}


				
			

This example demonstrates the factory method pattern where different concrete factories (CarFactory and BicycleFactory) create objects of their corresponding concrete products (Car and Bicycle).

Builder Pattern

  • Intent: Separates the construction of a complex object from its use step by step.
  • Problem: Constructing a complex object with many parameters can lead to long and complex constructor functions. This can be difficult to read and maintain.
  • Solution: The Builder pattern introduces a separate builder class that provides a step-by-step approach to constructing a complex object. Clients can call methods on the builder to set different parts of the object and then create the final object using a build() method.
				
					#include <iostream>
#include <string>
#include <vector>

// Product class
class Pizza {
public:
    void setDough(const std::string& dough) {
        dough_ = dough;
    }
    
    void setSauce(const std::string& sauce) {
        sauce_ = sauce;
    }
    
    void setToppings(const std::vector<std::string>& toppings) {
        toppings_ = toppings;
    }
    
    void display() const {
        std::cout << "Pizza with " << dough_ << " dough, " << sauce_ << " sauce, and toppings:";
        for (const auto& topping : toppings_) {
            std::cout << " " << topping;
        }
        std::cout << std::endl;
    }

private:
    std::string dough_;
    std::string sauce_;
    std::vector<std::string> toppings_;
};

// Abstract Builder class
class PizzaBuilder {
public:
    virtual void buildDough() = 0;
    virtual void buildSauce() = 0;
    virtual void buildToppings() = 0;
    virtual Pizza* getPizza() = 0;
};

// Concrete Builder for Margherita Pizza
class MargheritaPizzaBuilder : public PizzaBuilder {
public:
    MargheritaPizzaBuilder() {
        pizza_ = new Pizza();
    }
    
    void buildDough() override {
        pizza_->setDough("thin");
    }
    
    void buildSauce() override {
        pizza_->setSauce("tomato");
    }
    
    void buildToppings() override {
        pizza_->setToppings({"cheese", "basil"});
    }
    
    Pizza* getPizza() override {
        return pizza_;
    }

private:
    Pizza* pizza_;
};

// Director class
class PizzaDirector {
public:
    Pizza* createPizza(PizzaBuilder* builder) {
        builder->buildDough();
        builder->buildSauce();
        builder->buildToppings();
        return builder->getPizza();
    }
};

int main() {
    // Create a director
    PizzaDirector director;
    
    // Create a Margherita pizza builder
    MargheritaPizzaBuilder margheritaBuilder;
    
    // Build a Margherita pizza using the director
    Pizza* margheritaPizza = director.createPizza(&margheritaBuilder);
    
    // Display the Margherita pizza
    margheritaPizza->display();
    
    // Clean up memory
    delete margheritaPizza;
    
    return 0;
}

				
			

In this example, we have a Pizza class representing the product we want to build. We also have an abstract PizzaBuilder class defining the steps to build a pizza. Concrete builder classes (MargheritaPizzaBuilder) implement these steps to build specific types of pizzas. Finally, a PizzaDirector class constructs the pizza using the builder.

This pattern allows you to vary the internal representation of the product being constructed and allows you to reuse the same construction code to build different representations.

Common Structural Design Patterns

Adapter Pattern

  • Intent: Allows incompatible interfaces to work together.
  • Problem: Sometimes, you need to use an existing class that has an incompatible interface with the rest of your code.
  • Solution: The Adapter pattern introduces an adapter class that acts as a wrapper around the existing class. The adapter provides a compatible interface that your code can use.
				
					#include <iostream>

// MediaPlayer interface
class MediaPlayer {
public:
    virtual void play(const std::string& audioType, const std::string& fileName) = 0;
};

// Concrete MediaPlayer implementation
class AudioPlayer : public MediaPlayer {
public:
    void play(const std::string& audioType, const std::string& fileName) override {
        if (audioType == "mp3") {
            std::cout << "Playing mp3 file: " << fileName << std::endl;
        } else {
            std::cout << "Unsupported audio format" << std::endl;
        }
    }
};

// AdvancedMediaPlayer interface
class AdvancedMediaPlayer {
public:
    virtual void playVideo(const std::string& fileName) = 0;
};

// Concrete AdvancedMediaPlayer implementation for mp4 format
class Mp4Player : public AdvancedMediaPlayer {
public:
    void playVideo(const std::string& fileName) override {
        std::cout << "Playing mp4 video file: " << fileName << std::endl;
    }
};

// Adapter class to make Mp4Player compatible with MediaPlayer
class MediaPlayerAdapter : public MediaPlayer {
public:
    MediaPlayerAdapter(AdvancedMediaPlayer* advancedMediaPlayer) : advancedMediaPlayer_(advancedMediaPlayer) {}
    
    void play(const std::string& audioType, const std::string& fileName) override {
        if (audioType == "mp4") {
            advancedMediaPlayer_->playVideo(fileName);
        } else {
            std::cout << "Unsupported audio format" << std::endl;
        }
    }

private:
    AdvancedMediaPlayer* advancedMediaPlayer_;
};

int main() {
    // Create an audio player
    MediaPlayer* audioPlayer = new AudioPlayer();
    audioPlayer->play("mp3", "song.mp3"); // Output: Playing mp3 file: song.mp3
    
    // Create an advanced media player for mp4 format
    AdvancedMediaPlayer* mp4Player = new Mp4Player();
    
    // Use the adapter to play mp4 files using the audio player
    MediaPlayer* adapter = new MediaPlayerAdapter(mp4Player);
    adapter->play("mp4", "video.mp4"); // Output: Playing mp4 video file: video.mp4
    
    // Clean up memory
    delete audioPlayer;
    delete mp4Player;
    delete adapter;
    
    return 0;
}

				
			

In this example, MediaPlayer represents the existing interface for playing audio files. We have a concrete implementation AudioPlayer. We introduce the AdvancedMediaPlayer interface and its concrete implementation Mp4Player for playing video files.

The MediaPlayerAdapter acts as an adapter that allows Mp4Player (which plays video files) to be used through the MediaPlayer interface. It translates the requests from MediaPlayer format to AdvancedMediaPlayer format, allowing the client code to play video files using the MediaPlayer interface.

Common Behavioral Design Patterns

Behavioral design patterns focus on communication and interaction between objects to achieve specific behaviors. Here are some commonly used patterns:

Observer Pattern

  • Intent: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
  • Problem: In some scenarios, you need objects to be notified about changes in other objects. Manually notifying all dependent objects can be cumbersome and error-prone.
  • Solution: The Observer pattern introduces a subject and observer interface. The subject maintains a list of observers and notifies them whenever its state changes. Observers implement an update method that is called by the subject when a notification is sent.
				
					#include <iostream>
#include <vector>
#include <algorithm>

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

// Concrete Observer class
class ConcreteObserver : public Observer {
public:
    ConcreteObserver(const std::string& name) : name_(name) {}
    
    void update(const std::string& message) override {
        std::cout << name_ << " received message: " << message << std::endl;
    }

private:
    std::string name_;
};

// Abstract Subject class
class Subject {
public:
    virtual void attach(Observer* observer) = 0;
    virtual void detach(Observer* observer) = 0;
    virtual void notify(const std::string& message) = 0;
};

// Concrete Subject class
class ConcreteSubject : public Subject {
public:
    void attach(Observer* observer) override {
        observers_.push_back(observer);
    }
    
    void detach(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);
        }
    }

private:
    std::vector
    <Observer*> observers_;
};

int main() {
    // Create concrete subject
    ConcreteSubject subject;

    // Create concrete observers
    ConcreteObserver observer1("Observer 1");
    ConcreteObserver observer2("Observer 2");

    // Attach observers to the subject
    subject.attach(&observer1);
    subject.attach(&observer2);

    // Notify observers
    subject.notify("Hello, observers!");

    // Detach an observer
    subject.detach(&observer2);

    // Notify remaining observer
    subject.notify("Observer 2 has been detached.");

    return 0;
}

				
			

In this example, Observer is an abstract class defining the interface for objects that should be notified of changes in the subject. ConcreteObserver is a concrete implementation of the observer, which receives and handles update notifications.

Subject is an abstract class defining the interface for attaching, detaching, and notifying observers. ConcreteSubject is a concrete implementation of the subject, which maintains a list of observers and notifies them of changes in state.

In the main() function, we create a concrete subject and two concrete observers. We attach both observers to the subject and then notify them. After that, we detach one observer and notify the remaining one.

Strategy Pattern

  • Intent: Defines a family of algorithms, encapsulates each one, and makes them interchangeable. The strategy allows the algorithm to be selected at runtime.
  • Problem: If your code has different algorithms for a specific task embedded within the code itself, it becomes difficult to change or add new algorithms later.
  • Solution: The Strategy pattern introduces a strategy interface that defines a common operation for all algorithms. Concrete strategy classes implement this interface, providing specific algorithm implementations. The client code can choose and use a particular strategy object at runtime.
				
					#include <iostream>
#include <vector>
#include <algorithm>

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

// Concrete strategy for bubble sort
class BubbleSort : public SortingStrategy {
public:
    void sort(std::vector<int>& data) const override {
        std::cout << "Sorting using Bubble Sort" << std::endl;
        std::sort(data.begin(), data.end());
    }
};

// Concrete strategy for merge sort
class MergeSort : public SortingStrategy {
public:
    void sort(std::vector<int>& data) const override {
        std::cout << "Sorting using Merge Sort" << std::endl;
        // Implementation of merge sort
        mergeSort(data, 0, data.size() - 1);
    }

private:
    void merge(std::vector<int>& data, int left, int middle, int right) const {
        int n1 = middle - left + 1;
        int n2 = right - middle;

        std::vector<int> L(n1), R(n2);

        for (int i = 0; i < n1; i++)
            L[i] = data[left + i];
        for (int j = 0; j < n2; j++)
            R[j] = data[middle + 1 + j];

        int i = 0, j = 0, k = left;
        while (i < n1 && j < n2) {
            if (L[i] <= R[j]) {
                data[k] = L[i];
                i++;
            } else {
                data[k] = R[j];
                j++;
            }
            k++;
        }

        while (i < n1) {
            data[k] = L[i];
            i++;
            k++;
        }

        while (j < n2) {
            data[k] = R[j];
            j++;
            k++;
        }
    }

    void mergeSort(std::vector<int>& data, int left, int right) const {
        if (left < right) {
            int middle = left + (right - left) / 2;

            mergeSort(data, left, middle);
            mergeSort(data, middle + 1, right);

            merge(data, left, middle, right);
        }
    }
};

// Context class
class Sorter {
public:
    Sorter(SortingStrategy* strategy) : strategy_(strategy) {}

    void setStrategy(SortingStrategy* strategy) {
        strategy_ = strategy;
    }

    void performSort(std::vector<int>& data) {
        if (strategy_) {
            strategy_->sort(data);
        } else {
            std::cout << "No sorting strategy set." << std::endl;
        }
    }

private:
    SortingStrategy* strategy_;
};

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

    // Create a context with BubbleSort strategy
    Sorter sorter(new BubbleSort());
    sorter.performSort(data);

    // Change the sorting strategy to MergeSort
    sorter.setStrategy(new MergeSort());
    sorter.performSort(data);

    return 0;
}

				
			

In this example, we have a SortingStrategy interface defining the method sort(), which all concrete sorting strategies must implement. We have two concrete sorting strategies, BubbleSort and MergeSort, each implementing their own sorting algorithm.

The Sorter class acts as a context and is responsible for sorting the data using the selected strategy. It has a method performSort() that delegates the sorting task to the strategy object set within it.

In the main() function, we demonstrate how the sorting strategy can be changed dynamically by creating a Sorter object with one strategy and later setting another strategy. This demonstrates the interchangeability of sorting algorithms at runtime, which is a key feature of the Strategy Pattern.

Command Pattern

  • Intent: Encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and undo or redo requests.
  • Problem: Sometimes, you need to store or queue up actions to be executed later. Additionally, providing functionality to undo or redo actions might be desirable. If you directly call functions representing actions, these functionalities become difficult to implement.
  • Solution: The Command pattern introduces a command interface that defines a method to execute the encapsulated request. Concrete command classes implement this interface, encapsulating specific actions. The client code can create and store command objects, allowing for flexible execution, queuing, and potentially undo/redo functionality.
				
					#include <iostream>
#include <vector>

// Receiver class
class Receiver {
public:
    void performAction() {
        std::cout << "Action performed by the receiver" << std::endl;
    }

    void undoAction() {
        std::cout << "Undoing the action performed by the receiver" << std::endl;
    }
};

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

// Concrete command class
class ConcreteCommand : public Command {
public:
    ConcreteCommand(Receiver* receiver) : receiver_(receiver) {}

    void execute() override {
        receiver_->performAction();
    }

    void undo() override {
        receiver_->undoAction();
    }

private:
    Receiver* receiver_;
};

// Invoker class
class Invoker {
public:
    void setCommand(Command* command) {
        command_ = command;
    }

    void executeCommand() {
        command_->execute();
    }

    void undoCommand() {
        command_->undo();
    }

private:
    Command* command_;
};

int main() {
    // Create a receiver
    Receiver receiver;

    // Create a concrete command with the receiver
    ConcreteCommand concreteCommand(&receiver);

    // Create an invoker and set the command
    Invoker invoker;
    invoker.setCommand(&concreteCommand);

    // Execute the command
    invoker.executeCommand();

    // Undo the command
    invoker.undoCommand();

    return 0;
}

				
			

In this example, we have a Receiver class that performs the actual actions. The Command interface declares methods for executing and undoing commands. The ConcreteCommand class implements the Command interface and holds a reference to the Receiver object. It executes the action by calling the appropriate method on the receiver.

The Invoker class holds a reference to a command and invokes its methods. It decouples the sender of a request from the receiver and allows for parameterization and queuing of requests.

In the main() function, we create a receiver and a concrete command with that receiver. We then create an invoker and set the concrete command. Finally, we execute the command and then undo it.

key takeaways

  • Design patterns provide a general approach to solving common programming problems.
  • Applying design patterns can improve code reusability, maintainability, and communication.
  • Choose the appropriate design pattern based on the specific problem you are trying to solve.

Design patterns are valuable tools for writing well-structured, reusable, and maintainable C++ code. By understanding these patterns and their applications, you can improve the design and quality of your software projects.This explanation covered a foundational set of design patterns, including creational, structural, and behavioral patterns. Remember that there are many other design patterns available, each with its own specific purpose and benefits. As you continue to learn and develop your C++ skills, explore additional patterns to enhance your design capabilities. Happy coding !❤️

Table of Contents

Contact here

Copyright © 2025 Diginode

Made with ❤️ in India