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.
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.
Design patterns are typically categorized into three main groups
#include
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:
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.main()
function demonstrates how to use the singleton by obtaining its instance and invoking a member function (showMessage()
in this case).
#include
// 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
).
build()
method.
#include
#include
#include
// 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& 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 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.
#include
// 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.
Behavioral design patterns focus on communication and interaction between objects to achieve specific behaviors. Here are some commonly used patterns:
#include
#include
#include
// 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
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.
#include
#include
#include
// Strategy interface
class SortingStrategy {
public:
virtual void sort(std::vector& data) const = 0;
};
// Concrete strategy for bubble sort
class BubbleSort : public SortingStrategy {
public:
void sort(std::vector& 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& 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& data, int left, int middle, int right) const {
int n1 = middle - left + 1;
int n2 = right - middle;
std::vector 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& 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& data) {
if (strategy_) {
strategy_->sort(data);
} else {
std::cout << "No sorting strategy set." << std::endl;
}
}
private:
SortingStrategy* strategy_;
};
int main() {
std::vector 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.
#include
#include
// 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.
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 !❤️