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
- Principles and Guidelines
- Creational Patterns
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Structural Patterns
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Behavioral Patterns
- Observer
- Strategy
- Command
- Iterator
- Mediator
- Memento
- State
- Template Method
- Visitor
- C++ Tips and Idioms
- References & Further Reading
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_ptrfor lifetime management where appropriate. - Prefer value semantics for small objects and use
= default/= deletefor special member functions. - Use
std::functionfor general callbacks; prefer templates/inline functions for hot code paths. - Use RAII for resource management; avoid manual
new/deletein 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