54. Design Patterns in C++
Definition
Design patterns are general solutions for recurring software design problems. These patterns are not ready-made code — they are templates or guidelines for solving specific design problems. Understanding patterns helps developers communicate effectively about architecture and write code that is more maintainable, flexible, and reusable.
The four patterns covered here were chosen because of their high usage frequency and direct relevance to C++ development.
Singleton — A Single Global Instance
A Singleton ensures that only one instance of a class is created and provides global access to that instance.
Lazy Initialization
#include <iostream>
#include <string>
using namespace std;
class Database {
private:
static Database* instance;
string connectionString;
Database() : connectionString("mongodb://localhost:27017") {
cout << "Database initialized" << endl;
}
public:
static Database* getInstance() {
if (instance == nullptr) {
instance = new Database();
}
return instance;
}
void connect() {
cout << "Connected to: " << connectionString << endl;
}
void disconnect() {
cout << "Disconnected" << endl;
}
// Prevent copying
Database(const Database&) = delete;
Database& operator=(const Database&) = delete;
};
Database* Database::instance = nullptr;
int main() {
Database* db1 = Database::getInstance();
Database* db2 = Database::getInstance();
db1->connect();
cout << "Same instance: " << (db1 == db2 ? "Yes" : "No") << endl;
db2->disconnect();
return 0;
}
Code Explanation
- The constructor is private — you cannot create an instance from outside.
getInstance()is a static method that lazy-initializes the instance.- The copy constructor and assignment operator are deleted.
Meyer's Singleton (Thread-Safe, C++11)
#include <iostream>
#include <string>
using namespace std;
class Config {
private:
Config() {
cout << "Config loaded" << endl;
}
public:
static Config& getInstance() {
static Config instance; // initialized once, thread-safe in C++11
return instance;
}
string get(const string& key) {
if (key == "app_name") return "MyApp";
if (key == "version") return "1.0";
return "";
}
Config(const Config&) = delete;
Config& operator=(const Config&) = delete;
};
int main() {
cout << "App: " << Config::getInstance().get("app_name") << endl;
cout << "Version: " << Config::getInstance().get("version") << endl;
return 0;
}
Code Explanation
static Config instanceinside the function — initialized on the first call.- C++11 guarantees thread-safe initialization for static local variables.
- Simpler and safer than manual lazy initialization.
Case Study: Global Logger
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
class Logger {
private:
ofstream logFile;
Logger() {
logFile.open("app.log", ios::app);
}
public:
static Logger& getInstance() {
static Logger instance;
return instance;
}
void info(const string& msg) {
logFile << "[INFO] " << msg << endl;
cout << "[INFO] " << msg << endl;
}
void error(const string& msg) {
logFile << "[ERROR] " << msg << endl;
cout << "[ERROR] " << msg << endl;
}
~Logger() {
logFile.close();
}
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
};
int main() {
Logger::getInstance().info("Program started");
Logger::getInstance().info("Processing data");
Logger::getInstance().error("Something went wrong");
Logger::getInstance().info("Program ended");
return 0;
}
Conclusion
A Singleton is suitable for resources that must have a single instance: database connections, loggers, configuration managers. Use Meyer's singleton because it is simpler and thread-safe.
Factory Method — Object Creation
Factory Method separates object creation from client code — the client doesn't need to know the details of the concrete class being created.
Code Example
#include <iostream>
#include <memory>
#include <string>
#include <vector>
using namespace std;
// Interface
class Shape {
public:
virtual void draw() = 0;
virtual string getType() = 0;
virtual double area() = 0;
virtual ~Shape() = default;
};
// Concrete classes
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
void draw() override { cout << "Drawing circle r=" << radius << endl; }
string getType() override { return "Circle"; }
double area() override { return 3.14159 * radius * radius; }
};
class Rectangle : public Shape {
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
void draw() override { cout << "Drawing square " << width << "x" << height << endl; }
string getType() override { return "Rectangle"; }
double area() override { return width * height; }
};
class Triangle : public Shape {
double base, height;
public:
Triangle(double b, double h) : base(b), height(h) {}
void draw() override { cout << "Drawing triangle b=" << base << " h=" << height << endl; }
string getType() override { return "Triangle"; }
double area() override { return 0.5 * base * height; }
};
// Factory
class ShapeFactory {
public:
static unique_ptr<Shape> create(const string& type) {
if (type == "circle") return make_unique<Circle>(5.0);
if (type == "rectangle") return make_unique<Rectangle>(4.0, 6.0);
if (type == "triangle") return make_unique<Triangle>(3.0, 8.0);
return nullptr;
}
};
int main() {
vector<string> types = {"circle", "rectangle", "triangle"};
for (const auto& type : types) {
auto shape = ShapeFactory::create(type);
if (shape) {
shape->draw();
cout << " Area: " << shape->area() << endl;
}
}
return 0;
}
Code Explanation
Shapeis an abstract base class (interface).ShapeFactory::create()returns aunique_ptr<Shape>— polymorphic.- The client only needs to know the string type, not the concrete class.
Case Study: Notification System
#include <iostream>
#include <memory>
#include <string>
using namespace std;
class Notification {
public:
virtual void send(const string& message) = 0;
virtual ~Notification() = default;
};
class EmailNotification : public Notification {
string email;
public:
EmailNotification(const string& e) : email(e) {}
void send(const string& msg) override {
cout << "[Email to " << email << "] " << msg << endl;
}
};
class SMSNotification : public Notification {
string phone;
public:
SMSNotification(const string& p) : phone(p) {}
void send(const string& msg) override {
cout << "[SMS to " << phone << "] " << msg << endl;
}
};
class PushNotification : public Notification {
string deviceToken;
public:
PushNotification(const string& d) : deviceToken(d) {}
void send(const string& msg) override {
cout << "[Push to device " << deviceToken.substr(0, 8) << "...] " << msg << endl;
}
};
class NotificationFactory {
public:
static unique_ptr<Notification> create(const string& type, const string& target) {
if (type == "email") return make_unique<EmailNotification>(target);
if (type == "sms") return make_unique<SMSNotification>(target);
if (type == "push") return make_unique<PushNotification>(target);
return nullptr;
}
};
int main() {
auto email = NotificationFactory::create("email", "[email protected]");
auto sms = NotificationFactory::create("sms", "081234567890");
auto push = NotificationFactory::create("push", "abc123def456");
email->send("Welcome!");
sms->send("OTP code: 123456");
push->send("You have a new message");
return 0;
}
Conclusion
Factory Method isolates object creation, makes it easy to add new types without changing client code, and supports polymorphism.
Observer — Event System
Observer defines a one-to-many dependency — when the subject changes, all observers are automatically notified.
Code Example
#include <iostream>
#include <vector>
#include <string>
#include <functional>
#include <algorithm>
using namespace std;
// Event system using callbacks
class EventEmitter {
vector<pair<string, function<void(const string&)>>> listeners;
public:
void on(const string& event, function<void(const string&)> callback) {
listeners.push_back({event, callback});
}
void emit(const string& event, const string& data = "") {
for (auto& [eventType, callback] : listeners) {
if (eventType == event) {
callback(data);
}
}
}
void off(const string& event) {
listeners.erase(
remove_if(listeners.begin(), listeners.end(),
[&](const auto& pair) { return pair.first == event; }),
listeners.end()
);
}
};
// Subject — publisher
class StockPrice {
EventEmitter& emitter;
string symbol;
double price;
public:
StockPrice(EventEmitter& em, const string& sym, double p)
: emitter(em), symbol(sym), price(p) {}
void setPrice(double newPrice) {
double oldPrice = price;
price = newPrice;
emitter.emit("priceChanged",
symbol + ": " + to_string(oldPrice) + " -> " + to_string(newPrice));
}
double getPrice() const { return price; }
};
// Observers
void logObserver(const string& data) {
cout << "[LOG] " << data << endl;
}
void alertObserver(const string& data) {
cout << "[ALERT] Significant change: " << data << endl;
}
int main() {
EventEmitter emitter;
StockPrice stock(emitter, "AAPL", 150.0);
// Register observers
emitter.on("priceChanged", logObserver);
emitter.on("priceChanged", alertObserver);
// Price changes trigger the observers
stock.setPrice(155.0);
stock.setPrice(160.0);
// Remove the alert observer
emitter.off("priceChanged");
stock.setPrice(145.0);
return 0;
}
Code Explanation
EventEmittermanages a list of listeners per event.on()registers a callback for a specific event.emit()calls all registered callbacks.off()removes listeners.
Case Study: UI Notification System
#include <iostream>
#include <vector>
#include <functional>
#include <string>
using namespace std;
class EventBus {
vector<function<void(const string&, const string&)>> handlers;
public:
void subscribe(function<void(const string&, const string&)> handler) {
handlers.push_back(handler);
}
void publish(const string& channel, const string& message) {
for (auto& handler : handlers) {
handler(channel, message);
}
}
};
class UIElement {
string name;
public:
UIElement(const string& n) : name(n) {}
void update(const string& channel, const string& message) {
cout << "[" << name << "] Channel " << channel << ": " << message << endl;
}
};
int main() {
EventBus bus;
UIElement header("Header");
UIElement sidebar("Sidebar");
UIElement footer("Footer");
bus.subscribe([&](const string& ch, const string& msg) { header.update(ch, msg); });
bus.subscribe([&](const string& ch, const string& msg) { sidebar.update(ch, msg); });
bus.subscribe([&](const string& ch, const string& msg) { footer.update(ch, msg); });
cout << "--- User Login ---" << endl;
bus.publish("auth", "User logged in");
cout << "\n--- Theme Change ---" << endl;
bus.publish("theme", "Dark mode activated");
return 0;
}
Conclusion
The Observer pattern is ideal for event-driven systems, UI frameworks, and notification systems. The decoupling between publisher and subscriber makes it easy to add new handlers.
Strategy — Algorithm Selection
Strategy enables runtime selection of algorithms — replacing an object's behavior without changing its class.
Code Example
#include <iostream>
#include <memory>
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;
// Strategy interface
class SortStrategy {
public:
virtual void sort(vector<int>& data) = 0;
virtual string name() = 0;
virtual ~SortStrategy() = default;
};
// Concrete strategies
class BubbleSort : public SortStrategy {
public:
void sort(vector<int>& data) override {
int n = data.size();
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (data[j] > data[j + 1]) {
swap(data[j], data[j + 1]);
}
}
}
}
string name() override { return "Bubble Sort"; }
};
class QuickSort : public SortStrategy {
public:
void sort(vector<int>& data) override {
std::sort(data.begin(), data.end());
}
string name() override { return "Quick Sort (std::sort)"; }
};
class InsertionSort : public SortStrategy {
public:
void sort(vector<int>& data) override {
for (size_t i = 1; i < data.size(); i++) {
int key = data[i];
int j = i - 1;
while (j >= 0 && data[j] > key) {
data[j + 1] = data[j];
j--;
}
data[j + 1] = key;
}
}
string name() override { return "Insertion Sort"; }
};
// Context
class Sorter {
unique_ptr<SortStrategy> strategy;
public:
void setStrategy(unique_ptr<SortStrategy> s) {
strategy = move(s);
}
void performSort(vector<int>& data) {
if (strategy) {
cout << "Using: " << strategy->name() << endl;
strategy->sort(data);
}
}
};
void print(const vector<int>& data) {
for (int val : data) cout << val << " ";
cout << endl;
}
int main() {
vector<int> data1 = {64, 34, 25, 12, 22, 11, 90};
vector<int> data2 = data1;
vector<int> data3 = data1;
Sorter sorter;
// Strategy 1: Bubble Sort
sorter.setStrategy(make_unique<BubbleSort>());
sorter.performSort(data1);
cout << "Result: ";
print(data1);
// Strategy 2: Quick Sort
sorter.setStrategy(make_unique<QuickSort>());
sorter.performSort(data2);
cout << "Result: ";
print(data2);
// Strategy 3: Insertion Sort
sorter.setStrategy(make_unique<InsertionSort>());
sorter.performSort(data3);
cout << "Result: ";
print(data3);
return 0;
}
Code Explanation
SortStrategyis the interface for all sorting algorithms.Sorteris the context that can swap strategies at any time.- The client chooses a strategy based on runtime needs.
Case Study: Payment System
#include <iostream>
#include <memory>
#include <string>
using namespace std;
class PaymentStrategy {
public:
virtual bool pay(double amount) = 0;
virtual string method() = 0;
virtual ~PaymentStrategy() = default;
};
class CreditCardPayment : public PaymentStrategy {
string cardNumber;
public:
CreditCardPayment(const string& cn) : cardNumber(cn) {}
bool pay(double amount) override {
cout << "Paying $" << amount << " with credit card "
<< cardNumber.substr(0, 4) << "..." << endl;
return true;
}
string method() override { return "Credit Card"; }
};
class GoPayPayment : public PaymentStrategy {
string phoneNumber;
public:
GoPayPayment(const string& pn) : phoneNumber(pn) {}
bool pay(double amount) override {
cout << "Paying $" << amount << " with GoPay "
<< phoneNumber.substr(0, 4) << "..." << endl;
return true;
}
string method() override { return "GoPay"; }
};
class BankTransferPayment : public PaymentStrategy {
string accountNumber;
public:
BankTransferPayment(const string& an) : accountNumber(an) {}
bool pay(double amount) override {
cout << "Paying $" << amount << " via transfer to "
<< accountNumber.substr(0, 4) << "..." << endl;
return true;
}
string method() override { return "Transfer Bank"; }
};
class Checkout {
unique_ptr<PaymentStrategy> payment;
public:
void setPayment(unique_ptr<PaymentStrategy> p) {
payment = move(p);
}
void processOrder(double amount) {
if (payment) {
cout << "Metode: " << payment->method() << endl;
if (payment->pay(amount)) {
cout << "Payment successful!" << endl;
}
}
}
};
int main() {
Checkout checkout;
checkout.setPayment(make_unique<CreditCardPayment>("4111111111111234"));
checkout.processOrder(250000);
cout << endl;
checkout.setPayment(make_unique<GoPayPayment>("081234567890"));
checkout.processOrder(150000);
cout << endl;
checkout.setPayment(make_unique<BankTransferPayment>("1234567890"));
checkout.processOrder(500000);
return 0;
}
Conclusion
The Strategy pattern enables dynamic replacement of algorithms or behaviors. It is very useful for payment systems, sorting algorithms, compression methods, and validation rules.
Comparing the Patterns
| Pattern | Purpose | Use Cases |
|---|---|---|
| Singleton | A single global instance | Logger, Config, DB connection |
| Factory | Create objects without class details | Notification, Shape, Plugin |
| Observer | Many-to-many notification | Event system, UI updates |
| Strategy | Swap algorithms at runtime | Sorting, Payment, Validation |
Summary
Design patterns are an important toolbox for C++ developers. Singleton ensures a single instance, Factory separates object creation, Observer manages event subscription, and Strategy allows dynamic algorithm replacement. Combining these patterns creates a clean, maintainable, and flexible architecture.