Module 06: Design Patterns
Goal: Learn the most important design patterns — Creational, Structural, and Behavioral. Time: 3 days of focused study Prerequisites: Module 01-05
Table of Contents
- What Are Design Patterns?
- Creational Patterns
- Structural Patterns
- Behavioral Patterns
- Pattern Selection Guide
- Interview Questions
1. What Are Design Patterns?
Design patterns are reusable solutions to common design problems. They're not code — they're templates for solving problems that occur repeatedly in software design.
Three Categories:
CREATIONAL — How objects are created
Singleton, Factory, Abstract Factory, Builder, Prototype
STRUCTURAL — How objects are composed/structured
Adapter, Decorator, Proxy, Facade, Composite, Bridge, Flyweight
BEHAVIORAL — How objects communicate/interact
Observer, Strategy, Command, Template Method, Iterator, State, Chain of Responsibility2. Creational Patterns
Singleton
Intent: Ensure a class has only one instance and provide a global point of access.
// ✅ Thread-safe Singleton (Meyers' Singleton — C++11)
class Database {
string connectionString;
// Private constructor — can't create instances from outside
Database() : connectionString("default-connection") {
cout << "Database initialized" << endl;
}
// Delete copy and move
Database(const Database&) = delete;
Database& operator=(const Database&) = delete;
public:
// Static method returns the SINGLE instance
static Database& getInstance() {
static Database instance; // Created once, thread-safe in C++11
return instance;
}
void query(const string& sql) {
cout << "Executing: " << sql << endl;
}
};
int main() {
// Both references point to the SAME object
Database& db1 = Database::getInstance();
Database& db2 = Database::getInstance();
db1.query("SELECT * FROM users");
// Database db3; // ❌ Can't construct — private constructor
// Database db4 = db1; // ❌ Can't copy — deleted
cout << (&db1 == &db2) << endl; // 1 (true — same instance!)
}WHEN TO USE:
✅ Database connection pools
✅ Configuration managers
✅ Logger
✅ Thread pool
WHEN NOT TO USE:
❌ When testability matters (hard to mock singletons)
❌ When you might need multiple instances later
❌ Global mutable state causes hidden dependencies
WARNING: Singleton is often called an "anti-pattern" because:
- It introduces global state
- Makes unit testing hard (can't substitute with mock)
- Creates hidden dependencies
Consider Dependency Injection instead.Factory Method
Intent: Define an interface for creating objects, but let subclasses decide which class to instantiate.
// Product interface
class Button {
public:
virtual void render() = 0;
virtual void onClick() = 0;
virtual ~Button() = default;
};
// Concrete products
class WindowsButton : public Button {
public:
void render() override { cout << "Rendering Windows button" << endl; }
void onClick() override { cout << "Windows button clicked" << endl; }
};
class MacButton : public Button {
public:
void render() override { cout << "Rendering Mac button" << endl; }
void onClick() override { cout << "Mac button clicked" << endl; }
};
class LinuxButton : public Button {
public:
void render() override { cout << "Rendering Linux button" << endl; }
void onClick() override { cout << "Linux button clicked" << endl; }
};
// Creator with factory method
class Dialog {
public:
// FACTORY METHOD — subclasses override this
virtual unique_ptr<Button> createButton() = 0;
void render() {
auto button = createButton(); // Factory method creates the right button
button->render();
button->onClick();
}
virtual ~Dialog() = default;
};
class WindowsDialog : public Dialog {
public:
unique_ptr<Button> createButton() override {
return make_unique<WindowsButton>();
}
};
class MacDialog : public Dialog {
public:
unique_ptr<Button> createButton() override {
return make_unique<MacButton>();
}
};
// Simple factory function (not the pattern, but commonly used)
unique_ptr<Button> createButton(const string& os) {
if (os == "windows") return make_unique<WindowsButton>();
if (os == "mac") return make_unique<MacButton>();
if (os == "linux") return make_unique<LinuxButton>();
throw invalid_argument("Unknown OS: " + os);
}Abstract Factory
Intent: Create families of related objects without specifying concrete classes.
// Abstract products
class IButton { public: virtual void paint() = 0; virtual ~IButton() = default; };
class ICheckbox { public: virtual void check() = 0; virtual ~ICheckbox() = default; };
class ITextBox { public: virtual void type() = 0; virtual ~ITextBox() = default; };
// Windows family
class WinButton : public IButton { public: void paint() override { cout << "[Win Button]"; } };
class WinCheckbox : public ICheckbox { public: void check() override { cout << "[Win Checkbox]"; } };
class WinTextBox : public ITextBox { public: void type() override { cout << "[Win TextBox]"; } };
// Mac family
class MacButton : public IButton { public: void paint() override { cout << "[Mac Button]"; } };
class MacCheckbox : public ICheckbox { public: void check() override { cout << "[Mac Checkbox]"; } };
class MacTextBox : public ITextBox { public: void type() override { cout << "[Mac TextBox]"; } };
// Abstract Factory
class IGUIFactory {
public:
virtual unique_ptr<IButton> createButton() = 0;
virtual unique_ptr<ICheckbox> createCheckbox() = 0;
virtual unique_ptr<ITextBox> createTextBox() = 0;
virtual ~IGUIFactory() = default;
};
class WindowsFactory : public IGUIFactory {
public:
unique_ptr<IButton> createButton() override { return make_unique<WinButton>(); }
unique_ptr<ICheckbox> createCheckbox() override { return make_unique<WinCheckbox>(); }
unique_ptr<ITextBox> createTextBox() override { return make_unique<WinTextBox>(); }
};
class MacFactory : public IGUIFactory {
public:
unique_ptr<IButton> createButton() override { return make_unique<MacButton>(); }
unique_ptr<ICheckbox> createCheckbox() override { return make_unique<MacCheckbox>(); }
unique_ptr<ITextBox> createTextBox() override { return make_unique<MacTextBox>(); }
};
// Client code — doesn't know which OS it's creating for!
void buildUI(IGUIFactory& factory) {
auto button = factory.createButton();
auto checkbox = factory.createCheckbox();
auto textbox = factory.createTextBox();
button->paint();
checkbox->check();
textbox->type();
}Builder
Intent: Construct complex objects step by step, separating construction from representation.
class Pizza {
public:
string dough, sauce, cheese;
vector<string> toppings;
bool extraCheese = false;
void describe() const {
cout << "Pizza: " << dough << " dough, " << sauce << " sauce, "
<< cheese << " cheese" << endl;
for (const auto& t : toppings)
cout << " + " << t << endl;
}
};
class PizzaBuilder {
Pizza pizza;
public:
PizzaBuilder& setDough(const string& d) {
pizza.dough = d;
return *this;
}
PizzaBuilder& setSauce(const string& s) {
pizza.sauce = s;
return *this;
}
PizzaBuilder& setCheese(const string& c) {
pizza.cheese = c;
return *this;
}
PizzaBuilder& addTopping(const string& t) {
pizza.toppings.push_back(t);
return *this;
}
PizzaBuilder& withExtraCheese() {
pizza.extraCheese = true;
return *this;
}
Pizza build() {
return pizza;
}
};
int main() {
Pizza p = PizzaBuilder()
.setDough("thin crust")
.setSauce("marinara")
.setCheese("mozzarella")
.addTopping("mushrooms")
.addTopping("olives")
.withExtraCheese()
.build();
p.describe();
}
// WHEN TO USE:
// ✅ Object has many optional parameters
// ✅ Constructor would need too many parameters (telescoping constructor problem)
// ✅ Object needs step-by-step construction
// ✅ You want immutable objects with readable construction3. Structural Patterns
Adapter
Intent: Convert one interface into another that clients expect. Makes incompatible interfaces work together.
// Existing interface your code uses
class IMediaPlayer {
public:
virtual void play(const string& filename) = 0;
virtual ~IMediaPlayer() = default;
};
// Third-party library with a DIFFERENT interface
class VLCLibrary {
public:
void vlc_open(const string& file) { cout << "VLC opening: " << file << endl; }
void vlc_play() { cout << "VLC playing..." << endl; }
void vlc_stop() { cout << "VLC stopped" << endl; }
};
// ADAPTER — wraps VLCLibrary to match IMediaPlayer interface
class VLCAdapter : public IMediaPlayer {
VLCLibrary vlc;
public:
void play(const string& filename) override {
vlc.vlc_open(filename); // Translate call
vlc.vlc_play();
}
};
// Client code doesn't know about VLCLibrary!
void playMusic(IMediaPlayer& player, const string& file) {
player.play(file); // Works with any IMediaPlayer
}
int main() {
VLCAdapter adapter;
playMusic(adapter, "song.mp3");
}
// Real-world: Wrapping a C library to work with your C++ interface
// Real-world: Adapting REST API client to your repository interfaceDecorator
Intent: Add responsibilities to objects dynamically without modifying them. Wraps objects.
// Base interface
class INotifier {
public:
virtual void send(const string& message) = 0;
virtual ~INotifier() = default;
};
// Concrete component
class BasicNotifier : public INotifier {
string recipient;
public:
BasicNotifier(string r) : recipient(r) {}
void send(const string& message) override {
cout << "Email to " << recipient << ": " << message << endl;
}
};
// Base decorator
class NotifierDecorator : public INotifier {
protected:
unique_ptr<INotifier> wrapped;
public:
NotifierDecorator(unique_ptr<INotifier> notifier) : wrapped(move(notifier)) {}
void send(const string& message) override {
wrapped->send(message); // Delegate to wrapped object
}
};
// Concrete decorators — add behavior!
class SMSDecorator : public NotifierDecorator {
string phone;
public:
SMSDecorator(unique_ptr<INotifier> n, string ph)
: NotifierDecorator(move(n)), phone(ph) {}
void send(const string& message) override {
NotifierDecorator::send(message); // Call wrapped
cout << "SMS to " << phone << ": " << message << endl; // Add behavior
}
};
class SlackDecorator : public NotifierDecorator {
string channel;
public:
SlackDecorator(unique_ptr<INotifier> n, string ch)
: NotifierDecorator(move(n)), channel(ch) {}
void send(const string& message) override {
NotifierDecorator::send(message);
cout << "Slack #" << channel << ": " << message << endl;
}
};
int main() {
// Stack decorators dynamically!
auto notifier = make_unique<BasicNotifier>("alice@test.com");
auto withSMS = make_unique<SMSDecorator>(move(notifier), "+1234567890");
auto withSlack = make_unique<SlackDecorator>(move(withSMS), "alerts");
withSlack->send("Server down!");
// Output:
// Email to alice@test.com: Server down!
// SMS to +1234567890: Server down!
// Slack #alerts: Server down!
}Proxy
Intent: Provide a surrogate or placeholder that controls access to another object.
class IImage {
public:
virtual void display() = 0;
virtual ~IImage() = default;
};
// Real object — expensive to create (loads image from disk)
class RealImage : public IImage {
string filename;
public:
RealImage(string fn) : filename(fn) {
cout << "Loading image from disk: " << filename << " (slow!)" << endl;
}
void display() override {
cout << "Displaying: " << filename << endl;
}
};
// PROXY — controls access, adds lazy loading
class ImageProxy : public IImage {
string filename;
unique_ptr<RealImage> realImage; // Created only when needed
public:
ImageProxy(string fn) : filename(fn) {}
void display() override {
// Lazy initialization — load only when first displayed
if (!realImage) {
realImage = make_unique<RealImage>(filename);
}
realImage->display();
}
};
int main() {
// Creating proxy is CHEAP — no disk I/O
ImageProxy img1("photo1.jpg");
ImageProxy img2("photo2.jpg");
ImageProxy img3("photo3.jpg");
// Only photo1 is loaded from disk
img1.display(); // Loads now, then displays
img1.display(); // Just displays (already loaded)
// img2 and img3 never loaded!
}
// Types of proxies:
// Virtual Proxy — lazy initialization (above)
// Protection Proxy — access control (check permissions)
// Logging Proxy — log all method calls
// Caching Proxy — cache results of expensive operations4. Behavioral Patterns
Observer
Intent: Define a one-to-many dependency so that when one object changes, all dependents are notified automatically.
#include <functional>
class IObserver {
public:
virtual void update(const string& event, const string& data) = 0;
virtual ~IObserver() = default;
};
class EventEmitter {
unordered_map<string, vector<IObserver*>> listeners;
public:
void subscribe(const string& event, IObserver* observer) {
listeners[event].push_back(observer);
}
void unsubscribe(const string& event, IObserver* observer) {
auto& vec = listeners[event];
vec.erase(remove(vec.begin(), vec.end(), observer), vec.end());
}
void emit(const string& event, const string& data = "") {
if (listeners.count(event)) {
for (auto* observer : listeners[event]) {
observer->update(event, data);
}
}
}
};
// Concrete observers
class Logger : public IObserver {
public:
void update(const string& event, const string& data) override {
cout << "[LOG] " << event << ": " << data << endl;
}
};
class EmailAlert : public IObserver {
public:
void update(const string& event, const string& data) override {
cout << "[EMAIL] Alert for " << event << ": " << data << endl;
}
};
class Dashboard : public IObserver {
public:
void update(const string& event, const string& data) override {
cout << "[DASHBOARD] Updated: " << event << " = " << data << endl;
}
};
// Subject (publisher)
class Store : public EventEmitter {
map<string, double> inventory;
public:
void addProduct(const string& name, double price) {
inventory[name] = price;
emit("product_added", name + " ($" + to_string(price) + ")");
}
void sellProduct(const string& name) {
if (inventory.count(name)) {
inventory.erase(name);
emit("product_sold", name);
if (inventory.empty()) emit("out_of_stock", "All items sold!");
}
}
};
int main() {
Store store;
Logger logger;
EmailAlert alert;
Dashboard dash;
store.subscribe("product_added", &logger);
store.subscribe("product_added", &dash);
store.subscribe("product_sold", &logger);
store.subscribe("out_of_stock", &alert);
store.addProduct("Laptop", 999.99);
store.sellProduct("Laptop");
}Strategy
Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable.
// Strategy interface
class ISortStrategy {
public:
virtual void sort(vector<int>& data) = 0;
virtual string name() const = 0;
virtual ~ISortStrategy() = default;
};
class BubbleSort : public ISortStrategy {
public:
void sort(vector<int>& data) override {
for (size_t i = 0; i < data.size(); i++)
for (size_t j = 0; j < data.size() - i - 1; j++)
if (data[j] > data[j+1]) swap(data[j], data[j+1]);
}
string name() const override { return "BubbleSort"; }
};
class QuickSort : public ISortStrategy {
void quicksort(vector<int>& arr, int low, int high) {
if (low < high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++)
if (arr[j] < pivot) swap(arr[++i], arr[j]);
swap(arr[i+1], arr[high]);
int pi = i + 1;
quicksort(arr, low, pi - 1);
quicksort(arr, pi + 1, high);
}
}
public:
void sort(vector<int>& data) override {
if (!data.empty()) quicksort(data, 0, data.size() - 1);
}
string name() const override { return "QuickSort"; }
};
// Context — uses strategy
class Sorter {
unique_ptr<ISortStrategy> strategy;
public:
void setStrategy(unique_ptr<ISortStrategy> s) {
strategy = move(s);
}
void sort(vector<int>& data) {
cout << "Sorting with " << strategy->name() << endl;
strategy->sort(data);
}
};
int main() {
Sorter sorter;
vector<int> data = {5, 2, 8, 1, 9, 3};
// Use BubbleSort for small data
sorter.setStrategy(make_unique<BubbleSort>());
sorter.sort(data);
// Switch to QuickSort for large data — at runtime!
data = {5, 2, 8, 1, 9, 3, 7, 4, 6, 0};
sorter.setStrategy(make_unique<QuickSort>());
sorter.sort(data);
}Command
Intent: Encapsulate a request as an object, allowing undo/redo, queuing, and logging.
class ICommand {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual string description() const = 0;
virtual ~ICommand() = default;
};
class TextEditor {
string text;
public:
void insertText(int pos, const string& str) {
text.insert(pos, str);
}
void deleteText(int pos, int len) {
text.erase(pos, len);
}
string getText() const { return text; }
};
class InsertCommand : public ICommand {
TextEditor& editor;
int position;
string textToInsert;
public:
InsertCommand(TextEditor& ed, int pos, string txt)
: editor(ed), position(pos), textToInsert(txt) {}
void execute() override { editor.insertText(position, textToInsert); }
void undo() override { editor.deleteText(position, textToInsert.length()); }
string description() const override { return "Insert '" + textToInsert + "'"; }
};
class DeleteCommand : public ICommand {
TextEditor& editor;
int position;
string deletedText;
int length;
public:
DeleteCommand(TextEditor& ed, int pos, int len)
: editor(ed), position(pos), length(len) {}
void execute() override {
deletedText = editor.getText().substr(position, length);
editor.deleteText(position, length);
}
void undo() override { editor.insertText(position, deletedText); }
string description() const override { return "Delete " + to_string(length) + " chars"; }
};
// Command history for undo/redo
class CommandHistory {
vector<unique_ptr<ICommand>> history;
int current = -1;
public:
void execute(unique_ptr<ICommand> cmd) {
// Remove any redo history
history.resize(current + 1);
cmd->execute();
history.push_back(move(cmd));
current++;
}
void undo() {
if (current >= 0) {
history[current]->undo();
current--;
}
}
void redo() {
if (current + 1 < (int)history.size()) {
current++;
history[current]->execute();
}
}
};
int main() {
TextEditor editor;
CommandHistory history;
history.execute(make_unique<InsertCommand>(editor, 0, "Hello "));
history.execute(make_unique<InsertCommand>(editor, 6, "World!"));
cout << editor.getText() << endl; // "Hello World!"
history.undo();
cout << editor.getText() << endl; // "Hello "
history.redo();
cout << editor.getText() << endl; // "Hello World!"
}Template Method
Intent: Define the skeleton of an algorithm in a base class, letting subclasses override specific steps.
class DataMiner {
public:
// TEMPLATE METHOD — defines the algorithm's skeleton
void mine(const string& path) {
string rawData = openFile(path); // Step 1
string data = extractData(rawData); // Step 2 (varies)
string analysis = analyzeData(data); // Step 3 (varies)
generateReport(analysis); // Step 4
}
virtual ~DataMiner() = default;
protected:
// Default implementation
string openFile(const string& path) {
cout << "Opening: " << path << endl;
return "raw data from " + path;
}
// Abstract — subclasses MUST implement
virtual string extractData(const string& raw) = 0;
virtual string analyzeData(const string& data) = 0;
// Hook — subclasses CAN override
virtual void generateReport(const string& analysis) {
cout << "Report: " << analysis << endl;
}
};
class CSVMiner : public DataMiner {
protected:
string extractData(const string& raw) override {
return "CSV rows from " + raw;
}
string analyzeData(const string& data) override {
return "CSV analysis of " + data;
}
};
class JSONMiner : public DataMiner {
protected:
string extractData(const string& raw) override {
return "JSON objects from " + raw;
}
string analyzeData(const string& data) override {
return "JSON analysis of " + data;
}
};5. Pattern Selection Guide
PROBLEM │ PATTERN
──────────────────────────────────────┼────────────────
Need exactly one instance │ Singleton
Create objects without specifying type │ Factory Method
Create families of related objects │ Abstract Factory
Complex object step-by-step │ Builder
Make incompatible interfaces work │ Adapter
Add behavior dynamically │ Decorator
Control access or lazy-load │ Proxy
Notify multiple objects of changes │ Observer
Swap algorithms at runtime │ Strategy
Undo/redo, queue commands │ Command
Define algorithm skeleton │ Template Method6. Interview Questions
Q1: What is the Singleton pattern? What are its drawbacks?
Answer: Singleton ensures only one instance exists, with global access. In C++, use Meyers' Singleton (local static variable). Drawbacks: introduces global state, makes testing hard (can't mock), creates hidden dependencies, and can cause issues with destruction order. Prefer DI over Singleton when possible.
Q2: What is the Factory Method pattern? How is it different from Abstract Factory?
Answer: Factory Method defines an interface for creating ONE object, letting subclasses decide the type. Abstract Factory creates FAMILIES of related objects. Factory Method uses inheritance (subclass overrides creation method). Abstract Factory uses composition (inject a factory object). Use Factory for one product; Abstract Factory for product families.
Q3: What is the Observer pattern? Give a real-world example.
Answer: Observer defines a one-to-many relationship where when a subject changes state, all observers are notified automatically. Real-world: event systems (GUI button clicks), message brokers (pub/sub), MVC (model notifies views). Implementation: subject maintains a list of observers and calls their update() method when state changes.
Q4: What is the Strategy pattern? How does it differ from Template Method?
Answer: Strategy encapsulates interchangeable algorithms as separate classes and lets the client swap them at runtime via composition. Template Method defines an algorithm skeleton in a base class with virtual steps that subclasses override. Strategy uses composition ("has-a"); Template Method uses inheritance ("is-a"). Strategy swaps the entire algorithm; Template Method customizes steps.
Q5: Explain the Decorator pattern with an example.
Answer: Decorator dynamically adds responsibilities to an object by wrapping it. The decorator implements the same interface and delegates to the wrapped object while adding behavior. Example: a Notifier wrapped with SMSDecorator and SlackDecorator — each layer adds a notification channel. Unlike inheritance (static, single), decorators can be stacked dynamically in any combination.
Q6: What is the Builder pattern? When would you use it?
Answer: Builder constructs complex objects step-by-step, separating construction from representation. Use when: an object has many optional parameters (telescoping constructor problem), construction requires multiple steps, or you want readable code. Method chaining (.setX().setY().build()) makes construction clear.
Q7: What is the Command pattern? Give a use case.
Answer: Command encapsulates a request as an object with execute() and undo() methods. Use cases: undo/redo in text editors, macro recording, task queuing, transaction-based systems. Each command knows what it does AND how to undo it, enabling a command history stack.
Q8: What is the Proxy pattern? What types exist?
Answer: Proxy provides a surrogate that controls access to another object. Types: Virtual Proxy (lazy loading — create expensive object only when needed), Protection Proxy (access control — check permissions before delegating), Caching Proxy (cache results), Logging Proxy (log method calls), Remote Proxy (represent a remote object locally).
Q9: When would you use Adapter vs Decorator?
Answer: Adapter converts one interface to another (making incompatible things work together). Decorator adds new behavior to an existing interface (same interface in and out). Adapter changes the interface; Decorator enhances it. Use Adapter when integrating third-party libraries. Use Decorator when adding optional behavior dynamically.
Q10: How do design patterns relate to SOLID?
Answer: Strategy and Observer use DIP (depend on abstractions). Factory and Builder use OCP (extend without modifying). Interface Segregation shows in the focused interfaces patterns use. Decorator follows OCP (add behavior without changing existing code). Template Method uses SRP (each step is a separate responsibility). Patterns are implementations of SOLID principles.
Next Module: 07 - Advanced OOP — Composition vs Inheritance, CRTP, RAII, smart pointers, and advanced techniques.