Skip to content

Design Patterns Tutorial

This tutorial expands common software design patterns, explains why they matter, and includes concise C++ examples you can adapt. Each pattern includes: intent, problem it solves, consequences (pros/cons), when to use it, and a short idiomatic C++ snippet.

Table of Contents


Introduction

Design patterns are proven solutions to recurring design problems in software engineering. They capture best practices and provide a shared vocabulary for describing software structure and behavior. Patterns are not finished designs you copy verbatim; instead, they are templates you adapt to your context.

Principles and Guidelines

  • Separation of concerns: Keep responsibilities small and focused.
  • Encapsulation: Hide implementation details behind interfaces.
  • Program to an interface, not an implementation.
  • Prefer composition over inheritance when it improves flexibility.
  • SOLID: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion.

Creational Patterns (expanded)

Creational patterns deal with object creation mechanisms. They help make a system independent of how its objects are created, composed, and represented.

Singleton

Intent: Ensure a class has only one instance and provide a global point of access.

Problem: Some services (configuration, logging) are logically singletons. Naive global variables make testing and control difficult.

Consequences: Easy access to the instance; can introduce hidden dependencies and global state — hurts testability.

When to use: Rarely — prefer dependency injection. Useful for truly global resources where a single instance is required.

Thread-safe C++11 implementation:

class Logger {
public:
    static Logger& instance() {
        static Logger inst; // constructed on first use, thread-safe in C++11+
        return inst;
    }
    void log(const std::string& s) { /* write to sink */ }
private:
    Logger() = default;
    ~Logger() = default;
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
};

Notes: For testability provide an injectable interface ILogger and pass it to components.

Factory Method

Intent: Define an interface for creating an object, but let subclasses decide which concrete class to instantiate.

Problem: A class wants to delegate the creation of instances to subclasses.

Consequences: Simplifies extension; creation logic is centralized in subclasses.

Example:

struct Product { virtual ~Product() = default; virtual std::string name() const = 0; };
struct ConcreteA : Product { std::string name() const override { return "A"; } };
struct Creator { virtual std::unique_ptr<Product> factory() const = 0; virtual ~Creator() = default; };
struct CreatorA : Creator { std::unique_ptr<Product> factory() const override { return std::make_unique<ConcreteA>(); } };

Usage: clients use Creator and get products without depending on concrete classes.

Abstract Factory

Intent: Provide an interface for creating families of related or dependent objects without specifying their concrete classes.

Problem: Product families must be used together and you want to enforce consistent product variants.

Consequences: Makes it easy to swap entire product families; adds abstraction and potential complexity.

Example (GUI toolkit family):

struct Button { virtual void paint() = 0; virtual ~Button() = default; };
struct Checkbox { virtual void toggle() = 0; virtual ~Checkbox() = default; };
struct GUIFactory { virtual std::unique_ptr<Button> createButton() = 0; virtual std::unique_ptr<Checkbox> createCheckbox() = 0; virtual ~GUIFactory() = default; };
// Concrete factories implement platform-specific products.

Builder

Intent: Separate construction of a complex object from its representation.

Problem: Many optional parameters or a multi-step construction process.

Consequences: Cleaner construction code and fluent APIs; adds another object (the builder).

Example (fluent builder):

struct Pizza { std::string dough, sauce, topping; };
struct PizzaBuilder {
    Pizza p;
    PizzaBuilder& dough(std::string d){ p.dough = std::move(d); return *this; }
    PizzaBuilder& sauce(std::string s){ p.sauce = std::move(s); return *this; }
    PizzaBuilder& topping(std::string t){ p.topping = std::move(t); return *this; }
    Pizza build(){ return std::move(p); }
};

Prototype

Intent: Create new objects by copying a prototypical instance.

Problem: In some cases copying a prototypical object is cheaper than constructing it from scratch.

Consequences: Makes copies easy; careful with deep vs shallow copy semantics.

Example:

struct Prototype { virtual std::unique_ptr<Prototype> clone() const = 0; virtual ~Prototype() = default; };
struct Concrete : Prototype { int data; std::unique_ptr<Prototype> clone() const override { return std::make_unique<Concrete>(*this); } };

Structural Patterns (expanded)

Structural patterns describe ways to compose classes or objects to form larger structures while keeping them flexible and efficient.

Adapter

Intent: Convert the interface of a class into another interface clients expect.

Problem: Two classes have incompatible interfaces but must work together.

Consequences: Lets existing classes be reused without modification; can add little overhead.

Object adapter example:

class OldApi { public: void oldCall() { /* ... */ } };
class INew { public: virtual void request() = 0; virtual ~INew() = default; };
class Adapter : public INew {
    OldApi adaptee;
public:
    void request() override { adaptee.oldCall(); }
};

Bridge

Intent: Decouple an abstraction from its implementation so the two can vary independently.

Use when both the abstraction and implementation should be independently extensible.

Sketch:

struct Renderer { virtual void renderCircle(float x,float y,float r)=0; virtual ~Renderer()=default; };
struct APIShape { protected: Renderer& renderer; public: APIShape(Renderer& r):renderer(r){} virtual void draw()=0; };
// Concrete shapes delegate to renderer implementations.

Composite

Intent: Compose objects into tree structures to represent part-whole hierarchies; let clients treat individual objects and compositions uniformly.

Consequences: Simplifies client code; composite nodes and leaves share same interface.

Example sketch:

struct Component { virtual void operation() = 0; virtual ~Component()=default; };
struct Leaf : Component { void operation() override {/* leaf work */} };
struct Composite : Component { std::vector<std::unique_ptr<Component>> children; void add(std::unique_ptr<Component> c){ children.push_back(std::move(c)); } void operation() override { for(auto &c: children) c->operation(); } };

Decorator

Intent: Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

Example (IO-like decorator):

struct Stream { virtual void write(const std::string& s)=0; virtual ~Stream()=default; };
struct FileStream : Stream { void write(const std::string& s) override { /* write to file */ } };
struct BufferedStream : Stream { std::unique_ptr<Stream> inner; BufferedStream(std::unique_ptr<Stream> s):inner(std::move(s)){} void write(const std::string& s) override { /* buffer then inner->write */ } };

Facade

Intent: Provide a simplified interface to a complex subsystem.

Use when you want to reduce coupling between a client and many subsystem classes.

Flyweight

Intent: Share fine-grained objects to reduce memory usage when many similar objects are needed.

Example idea: share immutable intrinsic state (glyph shape) and keep extrinsic state (position) externally.

Proxy

Intent: Provide a surrogate for another object to control access (lazy initialization, access control, remote proxy).

Lazy (virtual) proxy example:

struct Image { virtual void draw() = 0; virtual ~Image()=default; };
struct RealImage : Image { RealImage(const std::string& path) { /* load */ } void draw() override { /* draw */ } };
struct ImageProxy : Image { std::string path; std::unique_ptr<RealImage> real; ImageProxy(std::string p):path(std::move(p)){} void draw() override { if(!real) real = std::make_unique<RealImage>(path); real->draw(); } };

Behavioral Patterns (expanded)

Behavioral patterns are about algorithms and object responsibilities: how objects interact and distribute work.

Observer

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

Problem: Objects need to notify others about state changes without tight coupling.

Consequences: Promotes decoupling between subject and observers; can lead to update cascades.

Example (subject with subscribe/notify using std::function):

#include <functional>
#include <vector>
#include <algorithm>

class Subject {
    std::vector<std::function<void(int)>> observers;
public:
    void subscribe(std::function<void(int)> obs){ observers.push_back(std::move(obs)); }
    void unsubscribe(const std::function<void(int)>& /*placeholder*/) { /* removal strategy omitted for brevity */ }
    void setValue(int v){ for(auto &o: observers) o(v); }
};

Notes: Real implementations need a way to remove observers and consider lifetime issues (weak references).

Strategy

Intent: Define a family of interchangeable algorithms and make them selectable at runtime.

Example:

struct Strategy { virtual int operate(int a,int b)=0; virtual ~Strategy()=default; };
struct Add : Strategy { int operate(int a,int b) override { return a+b; } };
struct Mul : Strategy { int operate(int a,int b) override { return a*b; } };
struct Context { std::unique_ptr<Strategy> s; int execute(int a,int b){ return s->operate(a,b); } };

Command

Intent: Encapsulate a request as an object, enabling parameterization, queuing, logging, and undo.

Example (simple command type):

struct Command { virtual void execute() = 0; virtual ~Command()=default; };
struct PrintCommand : Command { std::string m; PrintCommand(std::string s):m(std::move(s)){} void execute() override { std::puts(m.c_str()); } };

Undo support requires storing inverse operations or state snapshots.

Iterator

Intent: Provide a way to access elements of an aggregate sequentially without exposing its internal structure.

Example: use STL iterators; custom iterator implements operator++, operator*, comparison.

Mediator

Intent: Define an object that centralizes communication between a set of objects, reducing direct dependencies.

Use when many-to-many relationships between objects become complex to manage.

Memento

Intent: Capture and externalize an object's internal state so it can be restored later without violating encapsulation.

Example sketch: Memento holds a snapshot of state; Originator creates/restores memento; Caretaker stores mementos.

State

Intent: Allow an object to change its behavior when its internal state changes by delegating behavior to state objects.

Template Method

Intent: Define the skeleton of an algorithm in a method, deferring some steps to subclasses.

Example:

class Abstract {
public:
    void run(){ step1(); step2(); }
protected:
    virtual void step1() = 0;
    virtual void step2() = 0;
};

Visitor

Intent: Represent an operation to be performed on elements of an object structure without changing the elements.

Use when you need to add operations to a class hierarchy frequently.

Example idea: each element implements accept(Visitor&) and the visitor implements visit(ElementType&) overloads.


C++ Tips and Idioms

  • Prefer std::unique_ptr / std::shared_ptr for lifetime management where appropriate.
  • Prefer value semantics for small objects and use = default/= delete for special member functions.
  • Use std::function for general callbacks; prefer templates/inline functions for hot code paths.
  • Use RAII for resource management; avoid manual new/delete in library code.
  • Be mindful of object slicing; use pointers or references for polymorphism.

References & Further Reading

  • Design Patterns: Elements of Reusable Object-Oriented Software — Gamma, Helm, Johnson, Vlissides (Gang of Four)
  • Head First Design Patterns — Freeman & Robson (practical, example-driven)
  • Modern C++ Design — Andrei Alexandrescu (advanced generic programming)
  • Refactoring.Guru: https://refactoring.guru/design-patterns